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.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.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
|
{
|
|
@@ -671,19 +682,19 @@ const max = Math.max;
|
|
|
671
682
|
* @param {number} x
|
|
672
683
|
* @return {number}
|
|
673
684
|
* @memberof Math */
|
|
674
|
-
const sign = Math.sign;
|
|
685
|
+
const sign = (x) => Math.sign(x);
|
|
675
686
|
|
|
676
687
|
/** Returns hypotenuse of values passed in
|
|
677
688
|
* @param {...number} values
|
|
678
689
|
* @return {number}
|
|
679
690
|
* @memberof Math */
|
|
680
|
-
const hypot = Math.hypot;
|
|
691
|
+
const hypot = (...values) => Math.hypot(...values);
|
|
681
692
|
|
|
682
693
|
/** Returns log2 of value passed in
|
|
683
694
|
* @param {number} x
|
|
684
695
|
* @return {number}
|
|
685
696
|
* @memberof Math */
|
|
686
|
-
const log2 = Math.log2;
|
|
697
|
+
const log2 = (x) => Math.log2(x);
|
|
687
698
|
|
|
688
699
|
/** Returns sin of value passed in
|
|
689
700
|
* @param {number} x
|
|
@@ -825,7 +836,8 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
|
825
836
|
const dy = (posA.y - posB.y)*2;
|
|
826
837
|
const sx = sizeA.x + sizeB.x;
|
|
827
838
|
const sy = sizeA.y + sizeB.y;
|
|
828
|
-
|
|
839
|
+
// symmetric so isOverlapping(A,B) === isOverlapping(B,A) at touching edges
|
|
840
|
+
return abs(dx) < sx && abs(dy) < sy;
|
|
829
841
|
}
|
|
830
842
|
|
|
831
843
|
/** Returns true if a line segment is intersecting an axis aligned box
|
|
@@ -914,7 +926,7 @@ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'
|
|
|
914
926
|
/**
|
|
915
927
|
* Check if object is an array
|
|
916
928
|
* @param {any} a
|
|
917
|
-
* @return {
|
|
929
|
+
* @return {a is Array<any>}
|
|
918
930
|
* @memberof Math */
|
|
919
931
|
function isArray(a) { return Array.isArray(a); }
|
|
920
932
|
|
|
@@ -1052,7 +1064,13 @@ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length);
|
|
|
1052
1064
|
* @return {Vector2}
|
|
1053
1065
|
* @memberof Random */
|
|
1054
1066
|
function randInCircle(radius=1, minRadius=0)
|
|
1055
|
-
{
|
|
1067
|
+
{
|
|
1068
|
+
// r is uniform in area ⇒ r² uniform in [minRadius², radius²]
|
|
1069
|
+
// (the squared inner bound is what makes minRadius the actual exclusion edge)
|
|
1070
|
+
if (radius <= 0) return new Vector2;
|
|
1071
|
+
const ratio = clamp(minRadius / radius);
|
|
1072
|
+
return randVec2(radius * rand(ratio*ratio, 1)**.5);
|
|
1073
|
+
}
|
|
1056
1074
|
|
|
1057
1075
|
/** Returns a random color between the two passed in colors, combine components if linear
|
|
1058
1076
|
* @param {Color} [colorA=WHITE]
|
|
@@ -1122,7 +1140,14 @@ class RandomGenerator
|
|
|
1122
1140
|
* @param {number} [valueA]
|
|
1123
1141
|
* @param {number} [valueB]
|
|
1124
1142
|
* @return {number} */
|
|
1125
|
-
floatSign(valueA=1, valueB=0)
|
|
1143
|
+
floatSign(valueA=1, valueB=0)
|
|
1144
|
+
{
|
|
1145
|
+
const lo = min(valueA, valueB);
|
|
1146
|
+
const hi = max(valueA, valueB);
|
|
1147
|
+
const d = hi - lo;
|
|
1148
|
+
const e = this.float(d*2);
|
|
1149
|
+
return e < d ? lo + e : d - lo - e;
|
|
1150
|
+
}
|
|
1126
1151
|
|
|
1127
1152
|
/** Returns a random angle between -PI and PI
|
|
1128
1153
|
* @return {number} */
|
|
@@ -1835,9 +1860,15 @@ class Timer
|
|
|
1835
1860
|
* @return {number} */
|
|
1836
1861
|
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
1837
1862
|
|
|
1838
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
1863
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
1864
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
1839
1865
|
* @return {number} */
|
|
1840
|
-
getPercent()
|
|
1866
|
+
getPercent()
|
|
1867
|
+
{
|
|
1868
|
+
if (!this.isSet()) return 0;
|
|
1869
|
+
if (!this.setTime) return 1;
|
|
1870
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
1871
|
+
}
|
|
1841
1872
|
|
|
1842
1873
|
/** Get the time this timer was set to, returns 0 if not set
|
|
1843
1874
|
* @return {number} */
|
|
@@ -1864,9 +1895,9 @@ class Timer
|
|
|
1864
1895
|
* @memberof Utilities */
|
|
1865
1896
|
function formatTime(t)
|
|
1866
1897
|
{
|
|
1867
|
-
const
|
|
1898
|
+
const signStr = t < 0 ? '-' : '';
|
|
1868
1899
|
t = abs(t)|0;
|
|
1869
|
-
return
|
|
1900
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
1870
1901
|
}
|
|
1871
1902
|
|
|
1872
1903
|
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
@@ -1952,15 +1983,20 @@ function shareURL(title, url, callback)
|
|
|
1952
1983
|
function readSaveData(saveName, defaultSaveData)
|
|
1953
1984
|
{
|
|
1954
1985
|
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
1955
|
-
|
|
1956
|
-
//
|
|
1957
|
-
|
|
1986
|
+
|
|
1987
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
1988
|
+
// iframes) and corrupt JSON in stored data
|
|
1958
1989
|
let loadedData = {};
|
|
1959
|
-
|
|
1990
|
+
try
|
|
1960
1991
|
{
|
|
1961
|
-
|
|
1962
|
-
|
|
1992
|
+
const data = localStorage[saveName];
|
|
1993
|
+
if (data)
|
|
1994
|
+
{
|
|
1995
|
+
try { loadedData = JSON.parse(data); }
|
|
1996
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
1997
|
+
}
|
|
1963
1998
|
}
|
|
1999
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
1964
2000
|
return { ...defaultSaveData, ...loadedData };
|
|
1965
2001
|
}
|
|
1966
2002
|
|
|
@@ -1971,7 +2007,9 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
1971
2007
|
function writeSaveData(saveName, saveData)
|
|
1972
2008
|
{
|
|
1973
2009
|
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
1974
|
-
localStorage
|
|
2010
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
2011
|
+
try { localStorage[saveName] = JSON.stringify(saveData); }
|
|
2012
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
1975
2013
|
}
|
|
1976
2014
|
|
|
1977
2015
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -2066,7 +2104,7 @@ let canvasColorTiles = true;
|
|
|
2066
2104
|
|
|
2067
2105
|
/** Color to clear the canvas to before render, does not clear if alpha is 0
|
|
2068
2106
|
* @type {Color}
|
|
2069
|
-
* @memberof
|
|
2107
|
+
* @memberof Settings */
|
|
2070
2108
|
let canvasClearColor = CLEAR_BLACK;
|
|
2071
2109
|
|
|
2072
2110
|
/** The max size of the canvas, centered if window is larger
|
|
@@ -2266,7 +2304,8 @@ let touchInputEnable = true;
|
|
|
2266
2304
|
let touchGamepadEnable = false;
|
|
2267
2305
|
|
|
2268
2306
|
/** True if touch gamepad should have start button in the center
|
|
2269
|
-
* - Prevents activating
|
|
2307
|
+
* - Prevents activating within 2*touchGamepadSize of the virtual stick or face buttons
|
|
2308
|
+
* (one radius for the visible control + one radius of buffer beyond its edge)
|
|
2270
2309
|
* - When the game is paused, any touch will press the button
|
|
2271
2310
|
* - Set size to enable the center button
|
|
2272
2311
|
* @type {number}
|
|
@@ -2678,7 +2717,7 @@ class EngineObject
|
|
|
2678
2717
|
this.color = color.copy();
|
|
2679
2718
|
/** @property {Color} - Additive color to apply when rendered */
|
|
2680
2719
|
this.additiveColor = undefined;
|
|
2681
|
-
/** @property {boolean} - Should
|
|
2720
|
+
/** @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. */
|
|
2682
2721
|
this.mirror = false;
|
|
2683
2722
|
/** @property {boolean} - Has object been destroyed? */
|
|
2684
2723
|
this.destroyed = false;
|
|
@@ -2764,6 +2803,9 @@ class EngineObject
|
|
|
2764
2803
|
// child objects do not have physics
|
|
2765
2804
|
ASSERT(!this.parent);
|
|
2766
2805
|
|
|
2806
|
+
// bail if a collision callback destroyed us mid-frame
|
|
2807
|
+
if (this.destroyed) return;
|
|
2808
|
+
|
|
2767
2809
|
if (this.clampSpeed)
|
|
2768
2810
|
{
|
|
2769
2811
|
// limit max speed to prevent missing collisions
|
|
@@ -2819,6 +2861,8 @@ class EngineObject
|
|
|
2819
2861
|
|
|
2820
2862
|
// notify objects of collision and check if should be resolved
|
|
2821
2863
|
const collide1 = this.collideWithObject(o);
|
|
2864
|
+
// callback may have destroyed us; stop resolving against more objects
|
|
2865
|
+
if (this.destroyed) return;
|
|
2822
2866
|
const collide2 = o.collideWithObject(this);
|
|
2823
2867
|
if (!collide1 || !collide2) continue;
|
|
2824
2868
|
|
|
@@ -2914,13 +2958,17 @@ class EngineObject
|
|
|
2914
2958
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
2915
2959
|
if (isBlockedX)
|
|
2916
2960
|
{
|
|
2917
|
-
// try to
|
|
2961
|
+
// try to step over a 1-tile bump (direction follows gravity sign
|
|
2962
|
+
// so inverted gravity steps down off a ceiling bump instead of up;
|
|
2963
|
+
// zero gravity defaults to the normal-gravity step-up direction)
|
|
2918
2964
|
const epsilon = 1e-3;
|
|
2919
|
-
const
|
|
2920
|
-
const
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2965
|
+
const maxMove = .1;
|
|
2966
|
+
const gravitySign = gravity.y > 0 ? -1 : 1;
|
|
2967
|
+
const y = gravitySign > 0 ?
|
|
2968
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
|
|
2969
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
|
|
2970
|
+
const delta = abs(y - this.pos.y);
|
|
2971
|
+
if (delta < maxMove)
|
|
2924
2972
|
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
2925
2973
|
{
|
|
2926
2974
|
this.pos.y = y;
|
|
@@ -3060,6 +3108,8 @@ class EngineObject
|
|
|
3060
3108
|
* @return {EngineObject} The child object added */
|
|
3061
3109
|
addChild(child, localPos=vec2(), localAngle=0)
|
|
3062
3110
|
{
|
|
3111
|
+
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
3112
|
+
if (this.destroyed) return child;
|
|
3063
3113
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
3064
3114
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3065
3115
|
ASSERT(child !== this, 'cannot add self as child');
|
|
@@ -3076,10 +3126,7 @@ class EngineObject
|
|
|
3076
3126
|
removeChild(child)
|
|
3077
3127
|
{
|
|
3078
3128
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
3079
|
-
|
|
3080
|
-
const index = this.children.indexOf(child);
|
|
3081
|
-
ASSERT(index >= 0, 'child not found in children array');
|
|
3082
|
-
index >= 0 && this.children.splice(index, 1);
|
|
3129
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
3083
3130
|
child.parent = undefined;
|
|
3084
3131
|
}
|
|
3085
3132
|
|
|
@@ -3154,7 +3201,7 @@ class EngineObject
|
|
|
3154
3201
|
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
3155
3202
|
* - Primitive drawing for polygons, ellipses, and lines
|
|
3156
3203
|
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
3157
|
-
* - Text rendering with custom fonts and
|
|
3204
|
+
* - Text rendering with custom fonts and ImageFont support
|
|
3158
3205
|
* - Color and additive color blending for effects
|
|
3159
3206
|
* - Rotation, mirroring, and scaling transformations
|
|
3160
3207
|
* - Camera system with position, scale, and rotation
|
|
@@ -3250,7 +3297,7 @@ let primitiveCount;
|
|
|
3250
3297
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
3251
3298
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
3252
3299
|
* @memberof Draw */
|
|
3253
|
-
function tile(index=
|
|
3300
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3254
3301
|
{
|
|
3255
3302
|
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
3256
3303
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
@@ -3301,8 +3348,8 @@ class TileInfo
|
|
|
3301
3348
|
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
3302
3349
|
* @param {Vector2} [size] - Size of tile in pixels
|
|
3303
3350
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
3304
|
-
* @param {number} [padding] - How many pixels padding around
|
|
3305
|
-
* @param {number} [bleed] - How many pixels smaller to
|
|
3351
|
+
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
3352
|
+
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
3306
3353
|
*/
|
|
3307
3354
|
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3308
3355
|
{
|
|
@@ -3334,7 +3381,7 @@ class TileInfo
|
|
|
3334
3381
|
ASSERT(typeof frame === 'number');
|
|
3335
3382
|
const w = this.size.x + this.padding*2;
|
|
3336
3383
|
const x = frame*w;
|
|
3337
|
-
ASSERT(x
|
|
3384
|
+
ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
3338
3385
|
return this.offset(new Vector2(x));
|
|
3339
3386
|
}
|
|
3340
3387
|
|
|
@@ -3423,7 +3470,7 @@ class TextureInfo
|
|
|
3423
3470
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
3424
3471
|
* @memberof Draw */
|
|
3425
3472
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
3426
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
3473
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
3427
3474
|
{
|
|
3428
3475
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3429
3476
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -3466,10 +3513,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
3466
3513
|
}
|
|
3467
3514
|
else
|
|
3468
3515
|
{
|
|
3469
|
-
// untextured:
|
|
3470
|
-
//
|
|
3471
|
-
// uvs/rgba zeroed). Color+additive are folded together to match
|
|
3472
|
-
// the Canvas2D path's color.add(additiveColor) on line ~337.
|
|
3516
|
+
// untextured: fold color+additive to match the Canvas2D path's
|
|
3517
|
+
// color.add(additiveColor) on line ~337.
|
|
3473
3518
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
3474
3519
|
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
3475
3520
|
}
|
|
@@ -3682,7 +3727,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
3682
3727
|
* @param {boolean} [screenSpace]
|
|
3683
3728
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3684
3729
|
* @memberof Draw */
|
|
3685
|
-
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
3730
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3686
3731
|
{
|
|
3687
3732
|
ASSERT(isArray(points), 'points must be an array');
|
|
3688
3733
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -3731,7 +3776,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
3731
3776
|
* @param {boolean} [screenSpace]
|
|
3732
3777
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3733
3778
|
* @memberof Draw */
|
|
3734
|
-
function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
|
|
3779
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3735
3780
|
{
|
|
3736
3781
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
3737
3782
|
const size = vec2(width, halfDelta.length()*2);
|
|
@@ -3747,9 +3792,9 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
3747
3792
|
* @param {Vector2} [size=vec2(1)]
|
|
3748
3793
|
* @param {number} [sides]
|
|
3749
3794
|
* @param {Color} [color=WHITE]
|
|
3750
|
-
* @param {number} [angle]
|
|
3751
3795
|
* @param {number} [lineWidth]
|
|
3752
3796
|
* @param {Color} [lineColor=BLACK]
|
|
3797
|
+
* @param {number} [angle]
|
|
3753
3798
|
* @param {boolean} [useWebGL=glEnable]
|
|
3754
3799
|
* @param {boolean} [screenSpace]
|
|
3755
3800
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -3842,7 +3887,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
3842
3887
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3843
3888
|
|
|
3844
3889
|
// clamp line width to prevent artifacts
|
|
3845
|
-
lineWidth = clamp(lineWidth, 0,
|
|
3890
|
+
lineWidth = clamp(lineWidth, 0, min(size.x, size.y));
|
|
3846
3891
|
|
|
3847
3892
|
if (useWebGL && glEnable)
|
|
3848
3893
|
{
|
|
@@ -3884,24 +3929,26 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
3884
3929
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
3885
3930
|
}
|
|
3886
3931
|
|
|
3887
|
-
/** Draw
|
|
3932
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
3888
3933
|
* - Best when batched with other untextured polys
|
|
3889
3934
|
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
3890
3935
|
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
3891
3936
|
* @param {Vector2} pos
|
|
3892
|
-
* @param {
|
|
3937
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
3893
3938
|
* @param {Color} [colorInner=WHITE]
|
|
3894
3939
|
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
3940
|
+
* @param {number} [angle]
|
|
3895
3941
|
* @param {boolean} [useWebGL=glEnable]
|
|
3896
3942
|
* @param {boolean} [screenSpace]
|
|
3897
3943
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3898
3944
|
* @memberof Draw */
|
|
3899
|
-
let
|
|
3900
|
-
function
|
|
3945
|
+
let drawEllipseGradientOffset = 0;
|
|
3946
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3901
3947
|
{
|
|
3902
3948
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3903
|
-
ASSERT(
|
|
3949
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
3904
3950
|
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
3951
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3905
3952
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3906
3953
|
|
|
3907
3954
|
if (headlessMode) return;
|
|
@@ -3913,26 +3960,33 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
3913
3960
|
{
|
|
3914
3961
|
// convert to world space
|
|
3915
3962
|
pos = screenToWorld(pos);
|
|
3916
|
-
size
|
|
3963
|
+
size = size.scale(1/cameraScale);
|
|
3964
|
+
angle += cameraAngle;
|
|
3917
3965
|
}
|
|
3918
3966
|
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
3919
3967
|
// so back-to-back gradients at the same position have their hole
|
|
3920
3968
|
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
3921
3969
|
// verts and don't visibly stack
|
|
3922
3970
|
const sides = glCircleSides;
|
|
3923
|
-
const
|
|
3971
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
3924
3972
|
const innerInt = colorInner.rgbaInt();
|
|
3925
3973
|
const outerInt = colorOuter.rgbaInt();
|
|
3926
|
-
const offset =
|
|
3974
|
+
const offset = drawEllipseGradientOffset++;
|
|
3975
|
+
const c = cos(-angle), s = sin(-angle);
|
|
3976
|
+
const rim = (a) =>
|
|
3977
|
+
{
|
|
3978
|
+
const lx = sin(a)*radiusX, ly = cos(a)*radiusY;
|
|
3979
|
+
return vec2(pos.x + lx*c - ly*s, pos.y + lx*s + ly*c);
|
|
3980
|
+
};
|
|
3927
3981
|
const startA = (offset%sides)/sides*PI*2;
|
|
3928
|
-
const points = [
|
|
3982
|
+
const points = [rim(startA)];
|
|
3929
3983
|
const colors = [outerInt];
|
|
3930
3984
|
for (let i=sides; i--;)
|
|
3931
3985
|
{
|
|
3932
3986
|
const a = ((i+offset)%sides)/sides*PI*2;
|
|
3933
3987
|
points.push(pos);
|
|
3934
3988
|
colors.push(innerInt);
|
|
3935
|
-
points.push(
|
|
3989
|
+
points.push(rim(a));
|
|
3936
3990
|
colors.push(outerInt);
|
|
3937
3991
|
}
|
|
3938
3992
|
glDrawColoredPoints(points, colors);
|
|
@@ -3942,7 +3996,7 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
3942
3996
|
// normal canvas 2D rendering method (slower)
|
|
3943
3997
|
++drawCount;
|
|
3944
3998
|
++primitiveCount;
|
|
3945
|
-
drawCanvas2D(pos,
|
|
3999
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
3946
4000
|
{
|
|
3947
4001
|
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
3948
4002
|
gradient.addColorStop(0, colorInner.toString());
|
|
@@ -3955,6 +4009,24 @@ function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHIT
|
|
|
3955
4009
|
}
|
|
3956
4010
|
}
|
|
3957
4011
|
|
|
4012
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
4013
|
+
* - Best when batched with other untextured polys
|
|
4014
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
4015
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
4016
|
+
* @param {Vector2} pos
|
|
4017
|
+
* @param {number} [size=1] - Diameter
|
|
4018
|
+
* @param {Color} [colorInner=WHITE]
|
|
4019
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
4020
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
4021
|
+
* @param {boolean} [screenSpace]
|
|
4022
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4023
|
+
* @memberof Draw */
|
|
4024
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
4025
|
+
{
|
|
4026
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
4027
|
+
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
4028
|
+
}
|
|
4029
|
+
|
|
3958
4030
|
/**
|
|
3959
4031
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
3960
4032
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -4009,7 +4081,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4009
4081
|
* @param {number} [angle]
|
|
4010
4082
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
4011
4083
|
* @memberof Draw */
|
|
4012
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
4084
|
+
function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
4013
4085
|
{
|
|
4014
4086
|
// convert to screen space
|
|
4015
4087
|
pos = worldToScreen(pos);
|
|
@@ -4049,16 +4121,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
4049
4121
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
4050
4122
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4051
4123
|
|
|
4124
|
+
const lines = (text+'').split('\n');
|
|
4125
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4126
|
+
// save before style mutations so caller's context state is preserved
|
|
4127
|
+
context.save();
|
|
4052
4128
|
context.fillStyle = color.toString();
|
|
4053
4129
|
context.strokeStyle = lineColor.toString();
|
|
4054
4130
|
context.lineWidth = lineWidth;
|
|
4055
4131
|
context.textAlign = textAlign;
|
|
4056
4132
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
4057
4133
|
context.textBaseline = 'middle';
|
|
4058
|
-
|
|
4059
|
-
const lines = (text+'').split('\n');
|
|
4060
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4061
|
-
context.save();
|
|
4062
4134
|
context.translate(pos.x, posY);
|
|
4063
4135
|
context.rotate(-angle);
|
|
4064
4136
|
let yOffset = 0;
|
|
@@ -4219,6 +4291,9 @@ function isOnScreen(pos, size=0)
|
|
|
4219
4291
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4220
4292
|
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
4221
4293
|
|
|
4294
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
4295
|
+
if (!cameraScale) return false;
|
|
4296
|
+
|
|
4222
4297
|
// optimized circle on screen test
|
|
4223
4298
|
// pos = worldToScreen(pos);
|
|
4224
4299
|
let x = pos.x - cameraPos.x;
|
|
@@ -4260,7 +4335,10 @@ function combineCanvases()
|
|
|
4260
4335
|
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
4261
4336
|
workCanvas.width = w;
|
|
4262
4337
|
workCanvas.height = h;
|
|
4263
|
-
|
|
4338
|
+
// remove background alpha — explicit fillStyle so a previous caller
|
|
4339
|
+
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
4340
|
+
workContext.fillStyle = '#000';
|
|
4341
|
+
workContext.fillRect(0,0,w,h);
|
|
4264
4342
|
glCopyToContext(workContext);
|
|
4265
4343
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
4266
4344
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -4403,33 +4481,33 @@ function setCursor(cursorStyle = 'auto')
|
|
|
4403
4481
|
///////////////////////////////////////////////////////////////////////////////
|
|
4404
4482
|
|
|
4405
4483
|
/** Engine font image, 8x8 font provided by the engine
|
|
4406
|
-
* @type {
|
|
4484
|
+
* @type {ImageFont}
|
|
4407
4485
|
* @memberof Draw */
|
|
4408
|
-
let
|
|
4486
|
+
let engineImageFont;
|
|
4409
4487
|
|
|
4410
4488
|
/**
|
|
4411
|
-
* Font
|
|
4489
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
4412
4490
|
* - 96 characters (from space to tilde) are stored in an image
|
|
4413
4491
|
* - A 8x8 default engine font is supplied for general use
|
|
4414
4492
|
* - This system is WebGL enabled for fast text rendering
|
|
4415
4493
|
* - Fonts can also be colored and scaled along each axis
|
|
4416
|
-
*
|
|
4494
|
+
*
|
|
4417
4495
|
* @memberof Draw
|
|
4418
4496
|
* @example
|
|
4419
4497
|
* // use built in font
|
|
4420
|
-
* const font =
|
|
4498
|
+
* const font = engineImageFont;
|
|
4421
4499
|
*
|
|
4422
4500
|
* // draw text
|
|
4423
4501
|
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
4424
4502
|
*/
|
|
4425
|
-
class
|
|
4503
|
+
class ImageFont
|
|
4426
4504
|
{
|
|
4427
4505
|
/** Create an image font
|
|
4428
4506
|
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
4429
4507
|
*/
|
|
4430
4508
|
constructor(tileInfo)
|
|
4431
4509
|
{
|
|
4432
|
-
ASSERT(!!tileInfo, 'tileInfo is required for
|
|
4510
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
4433
4511
|
|
|
4434
4512
|
/** @property {TileInfo} - Tile info for the font */
|
|
4435
4513
|
this.tileInfo = tileInfo.frame(0);
|
|
@@ -4514,7 +4592,7 @@ class FontImage
|
|
|
4514
4592
|
}
|
|
4515
4593
|
|
|
4516
4594
|
// load engine font, called automatically on startup
|
|
4517
|
-
async function
|
|
4595
|
+
async function imageFontInit()
|
|
4518
4596
|
{
|
|
4519
4597
|
const image = new Image;
|
|
4520
4598
|
await new Promise(resolve =>
|
|
@@ -4527,7 +4605,7 @@ async function fontImageInit()
|
|
|
4527
4605
|
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
4528
4606
|
const textureInfo = new TextureInfo(image);
|
|
4529
4607
|
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
4530
|
-
|
|
4608
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
4531
4609
|
}
|
|
4532
4610
|
/**
|
|
4533
4611
|
* LittleJS Input System
|
|
@@ -4976,9 +5054,11 @@ function inputInit()
|
|
|
4976
5054
|
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
4977
5055
|
}
|
|
4978
5056
|
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
4979
|
-
function onMouseWheel(e)
|
|
4980
|
-
{
|
|
4981
|
-
|
|
5057
|
+
function onMouseWheel(e)
|
|
5058
|
+
{
|
|
5059
|
+
// accumulate so multiple wheel events in one frame are not lost
|
|
5060
|
+
if (!e.ctrlKey)
|
|
5061
|
+
mouseWheel += sign(e.deltaY);
|
|
4982
5062
|
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
4983
5063
|
e.preventDefault(); // prevent page scrolling
|
|
4984
5064
|
}
|
|
@@ -5103,9 +5183,14 @@ function inputInit()
|
|
|
5103
5183
|
if (button < touchGamepadButtonCount)
|
|
5104
5184
|
touchGamepadButtons[button] = 1;
|
|
5105
5185
|
}
|
|
5106
|
-
else if (startCenter.distance(touchPos)
|
|
5186
|
+
else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize &&
|
|
5187
|
+
stickCenter.distance(touchPos) >= 2 * touchGamepadSize &&
|
|
5188
|
+
buttonCenter.distance(touchPos) >= 2 * touchGamepadSize)
|
|
5107
5189
|
{
|
|
5108
5190
|
// virtual start button in center
|
|
5191
|
+
// require a fat-finger buffer of touchGamepadSize beyond the
|
|
5192
|
+
// edge of the stick/buttons so drift off those controls can't
|
|
5193
|
+
// accidentally fire start
|
|
5109
5194
|
touchGamepadButtons[9] = 1;
|
|
5110
5195
|
}
|
|
5111
5196
|
}
|
|
@@ -5147,7 +5232,7 @@ function inputUpdate()
|
|
|
5147
5232
|
v > min ? percent(v, min, max) :
|
|
5148
5233
|
v < -min ? -percent(-v, min, max) : 0;
|
|
5149
5234
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5150
|
-
}
|
|
5235
|
+
};
|
|
5151
5236
|
|
|
5152
5237
|
// update touch gamepad if enabled
|
|
5153
5238
|
if (touchGamepadEnable && isTouchDevice)
|
|
@@ -5161,7 +5246,12 @@ function inputUpdate()
|
|
|
5161
5246
|
debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5162
5247
|
debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5163
5248
|
if (touchGamepadCenterButtonSize)
|
|
5249
|
+
{
|
|
5164
5250
|
debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
|
|
5251
|
+
// exclusion bubbles around controls (where start is blocked)
|
|
5252
|
+
debugCircle(stickCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5253
|
+
debugCircle(buttonCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5254
|
+
}
|
|
5165
5255
|
}
|
|
5166
5256
|
|
|
5167
5257
|
if (!touchGamepadTimer.isSet()) return;
|
|
@@ -5268,13 +5358,6 @@ function inputUpdate()
|
|
|
5268
5358
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5269
5359
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5270
5360
|
}
|
|
5271
|
-
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
5272
|
-
{
|
|
5273
|
-
// digital style dpad from axes
|
|
5274
|
-
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
5275
|
-
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
5276
|
-
dpad.set(x, -y);
|
|
5277
|
-
}
|
|
5278
5361
|
|
|
5279
5362
|
// copy dpad to left analog stick when pressed
|
|
5280
5363
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
@@ -5480,7 +5563,7 @@ class Sound
|
|
|
5480
5563
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
5481
5564
|
this.onloadCallback = onloadCallback;
|
|
5482
5565
|
|
|
5483
|
-
if (
|
|
5566
|
+
if (isArray(asset))
|
|
5484
5567
|
{
|
|
5485
5568
|
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
5486
5569
|
const zzfxSound = asset.slice();
|
|
@@ -5572,7 +5655,7 @@ class Sound
|
|
|
5572
5655
|
}
|
|
5573
5656
|
|
|
5574
5657
|
/** Get how long this sound is in seconds
|
|
5575
|
-
* @return {number} - How long the sound is in seconds (
|
|
5658
|
+
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
5576
5659
|
*/
|
|
5577
5660
|
getDuration()
|
|
5578
5661
|
{ return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
|
|
@@ -5729,10 +5812,14 @@ class SoundInstance
|
|
|
5729
5812
|
{
|
|
5730
5813
|
if (fadeTime)
|
|
5731
5814
|
{
|
|
5732
|
-
// ramp off gain
|
|
5815
|
+
// ramp off gain from current volume (not 1, or low-volume
|
|
5816
|
+
// instances would jump back up before fading);
|
|
5817
|
+
// cancel any prior scheduling so stacked stop calls don't
|
|
5818
|
+
// re-anchor partway through a previous fade
|
|
5733
5819
|
const startFade = audioContext.currentTime;
|
|
5734
5820
|
const endFade = startFade + fadeTime;
|
|
5735
|
-
this.gainNode.gain.
|
|
5821
|
+
this.gainNode.gain.cancelScheduledValues(startFade);
|
|
5822
|
+
this.gainNode.gain.setValueAtTime(this.volume, startFade);
|
|
5736
5823
|
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
5737
5824
|
this.source.stop(endFade);
|
|
5738
5825
|
}
|
|
@@ -5780,13 +5867,14 @@ class SoundInstance
|
|
|
5780
5867
|
*/
|
|
5781
5868
|
getCurrentTime()
|
|
5782
5869
|
{
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5870
|
+
if (!this.isPlaying()) return this.pausedTime;
|
|
5871
|
+
const duration = this.getDuration();
|
|
5872
|
+
// guard mod against 0 duration (rate=0 or sound not loaded)
|
|
5873
|
+
return duration ? mod(audioContext.currentTime - this.startTime, duration) : 0;
|
|
5786
5874
|
}
|
|
5787
5875
|
|
|
5788
5876
|
/** Get the total duration of this sound
|
|
5789
|
-
* @return {number} - Total duration in seconds
|
|
5877
|
+
* @return {number} - Total duration in seconds (0 if loading)
|
|
5790
5878
|
*/
|
|
5791
5879
|
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
5792
5880
|
|
|
@@ -5810,7 +5898,7 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
5810
5898
|
{
|
|
5811
5899
|
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
5812
5900
|
if (!soundEnable || headlessMode) return;
|
|
5813
|
-
if (
|
|
5901
|
+
if (typeof speechSynthesis === 'undefined') return;
|
|
5814
5902
|
|
|
5815
5903
|
// common languages (not supported by all browsers)
|
|
5816
5904
|
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
@@ -5828,7 +5916,11 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
5828
5916
|
|
|
5829
5917
|
/** Stop all queued speech
|
|
5830
5918
|
* @memberof Audio */
|
|
5831
|
-
function speakStop()
|
|
5919
|
+
function speakStop()
|
|
5920
|
+
{
|
|
5921
|
+
if (typeof speechSynthesis !== 'undefined')
|
|
5922
|
+
speechSynthesis.cancel();
|
|
5923
|
+
}
|
|
5832
5924
|
|
|
5833
5925
|
/** Get frequency of a note on a musical scale
|
|
5834
5926
|
* @param {number} semitoneOffset - How many semitones away from the root note
|
|
@@ -5890,9 +5982,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
5890
5982
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
5891
5983
|
source.connect(pannerNode).connect(gainNode);
|
|
5892
5984
|
|
|
5893
|
-
//
|
|
5894
|
-
|
|
5895
|
-
|
|
5985
|
+
// disconnect nodes when the sound ends so the audio graph doesn't grow
|
|
5986
|
+
// unbounded across many play() calls (source.stop() also fires 'ended')
|
|
5987
|
+
source.addEventListener('ended', ()=>
|
|
5988
|
+
{
|
|
5989
|
+
gainNode.disconnect();
|
|
5990
|
+
pannerNode.disconnect();
|
|
5991
|
+
if (onended) onended(source);
|
|
5992
|
+
});
|
|
5896
5993
|
|
|
5897
5994
|
// play and return sound
|
|
5898
5995
|
const startOffset = offset * rate;
|
|
@@ -6129,14 +6226,29 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
|
6129
6226
|
* @memberof TileLayers */
|
|
6130
6227
|
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
6131
6228
|
{
|
|
6229
|
+
// check every layer and keep the closest hit so a far hit in an
|
|
6230
|
+
// earlier-registered layer doesn't shadow a closer hit in a later one
|
|
6231
|
+
let closestHit, closestDistSq, closestNormal;
|
|
6232
|
+
const scratchNormal = normal && vec2();
|
|
6132
6233
|
for (const layer of tileCollisionLayers)
|
|
6133
6234
|
{
|
|
6134
6235
|
if (!solidOnly || layer.isSolid)
|
|
6135
6236
|
{
|
|
6136
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject,
|
|
6137
|
-
if (hitPos)
|
|
6237
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, scratchNormal);
|
|
6238
|
+
if (hitPos)
|
|
6239
|
+
{
|
|
6240
|
+
const d = posStart.distanceSquared(hitPos);
|
|
6241
|
+
if (closestHit === undefined || d < closestDistSq)
|
|
6242
|
+
{
|
|
6243
|
+
closestHit = hitPos;
|
|
6244
|
+
closestDistSq = d;
|
|
6245
|
+
if (normal) closestNormal = scratchNormal.copy();
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6138
6248
|
}
|
|
6139
6249
|
}
|
|
6250
|
+
if (closestHit && normal) normal.setFrom(closestNormal);
|
|
6251
|
+
return closestHit;
|
|
6140
6252
|
}
|
|
6141
6253
|
|
|
6142
6254
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6310,38 +6422,6 @@ class CanvasLayer extends EngineObject
|
|
|
6310
6422
|
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
6311
6423
|
}
|
|
6312
6424
|
|
|
6313
|
-
/** Draw a tile onto the layer canvas in world space
|
|
6314
|
-
* @param {Vector2} pos
|
|
6315
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6316
|
-
* @param {TileInfo} [tileInfo]
|
|
6317
|
-
* @param {Color} [color=WHITE]
|
|
6318
|
-
* @param {number} [angle]
|
|
6319
|
-
* @param {boolean} [mirror] */
|
|
6320
|
-
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
6321
|
-
{
|
|
6322
|
-
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
6323
|
-
size = size.multiply(this.tileInfo.size);
|
|
6324
|
-
pos.y = this.canvas.height - pos.y;
|
|
6325
|
-
|
|
6326
|
-
// draw the tile onto the layer canvas
|
|
6327
|
-
const oldMainCanvasSize = mainCanvasSize;
|
|
6328
|
-
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
6329
|
-
const useWebGL = this.hasWebGL();
|
|
6330
|
-
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
6331
|
-
const drawContext = useWebGL ? undefined : this.context;
|
|
6332
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
6333
|
-
useWebGL && glSetRenderTarget();
|
|
6334
|
-
mainCanvasSize = oldMainCanvasSize;
|
|
6335
|
-
}
|
|
6336
|
-
|
|
6337
|
-
/** Draw a rectangle onto the layer canvas in world space
|
|
6338
|
-
* @param {Vector2} pos
|
|
6339
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6340
|
-
* @param {Color} [color=WHITE]
|
|
6341
|
-
* @param {number} [angle] */
|
|
6342
|
-
drawRect(pos, size, color, angle)
|
|
6343
|
-
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
6344
|
-
|
|
6345
6425
|
/** Create WebGL texture if necessary and copy layer canvas to it */
|
|
6346
6426
|
updateWebGL()
|
|
6347
6427
|
{ this.textureInfo.createWebGLTexture(); }
|
|
@@ -6397,6 +6477,8 @@ class TileLayer extends CanvasLayer
|
|
|
6397
6477
|
this.redrawTileData = ()=> {};
|
|
6398
6478
|
this.drawLayerTile = ()=> {};
|
|
6399
6479
|
this.drawLayerRect = ()=> {};
|
|
6480
|
+
this.drawTile = ()=> {};
|
|
6481
|
+
this.drawRect = ()=> {};
|
|
6400
6482
|
this.clearLayerRect = ()=> {};
|
|
6401
6483
|
return;
|
|
6402
6484
|
}
|
|
@@ -6424,7 +6506,7 @@ class TileLayer extends CanvasLayer
|
|
|
6424
6506
|
ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
|
|
6425
6507
|
|
|
6426
6508
|
if (!layerPos.arrayCheck(this.size)) return;
|
|
6427
|
-
this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
|
|
6509
|
+
this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] = data;
|
|
6428
6510
|
|
|
6429
6511
|
if (!redraw) return;
|
|
6430
6512
|
const isRedraw = drawContext === this.context;
|
|
@@ -6439,11 +6521,11 @@ class TileLayer extends CanvasLayer
|
|
|
6439
6521
|
|
|
6440
6522
|
/** Get data at a given position in the array
|
|
6441
6523
|
* @param {Vector2} layerPos - Local position in array
|
|
6442
|
-
* @return {TileLayerData} */
|
|
6524
|
+
* @return {TileLayerData|undefined} */
|
|
6443
6525
|
getData(layerPos)
|
|
6444
|
-
{
|
|
6526
|
+
{
|
|
6445
6527
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
6446
|
-
return layerPos.arrayCheck(this.size)
|
|
6528
|
+
return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
|
|
6447
6529
|
}
|
|
6448
6530
|
|
|
6449
6531
|
// Update the tile layer, refresh texture if needed
|
|
@@ -6551,7 +6633,7 @@ class TileLayer extends CanvasLayer
|
|
|
6551
6633
|
|
|
6552
6634
|
// draw the tile if it has layer data
|
|
6553
6635
|
const d = this.getData(layerPos);
|
|
6554
|
-
if (!d.tile) return;
|
|
6636
|
+
if (!d || !d.tile) return;
|
|
6555
6637
|
|
|
6556
6638
|
const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
|
|
6557
6639
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -6597,6 +6679,38 @@ class TileLayer extends CanvasLayer
|
|
|
6597
6679
|
drawLayerRect(pos, size, color, angle=0)
|
|
6598
6680
|
{ this.drawLayerTile(pos, size, undefined, color, angle); }
|
|
6599
6681
|
|
|
6682
|
+
/** Draw a tile onto the layer canvas in world space
|
|
6683
|
+
* @param {Vector2} pos
|
|
6684
|
+
* @param {Vector2} [size=vec2(1)]
|
|
6685
|
+
* @param {TileInfo} [tileInfo]
|
|
6686
|
+
* @param {Color} [color=WHITE]
|
|
6687
|
+
* @param {number} [angle]
|
|
6688
|
+
* @param {boolean} [mirror] */
|
|
6689
|
+
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
6690
|
+
{
|
|
6691
|
+
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
6692
|
+
size = size.multiply(this.tileInfo.size);
|
|
6693
|
+
pos.y = this.canvas.height - pos.y;
|
|
6694
|
+
|
|
6695
|
+
// draw the tile onto the layer canvas
|
|
6696
|
+
const oldMainCanvasSize = mainCanvasSize;
|
|
6697
|
+
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
6698
|
+
const useWebGL = this.hasWebGL();
|
|
6699
|
+
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
6700
|
+
const drawContext = useWebGL ? undefined : this.context;
|
|
6701
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
6702
|
+
useWebGL && glSetRenderTarget();
|
|
6703
|
+
mainCanvasSize = oldMainCanvasSize;
|
|
6704
|
+
}
|
|
6705
|
+
|
|
6706
|
+
/** Draw a rectangle onto the layer canvas in world space
|
|
6707
|
+
* @param {Vector2} pos
|
|
6708
|
+
* @param {Vector2} [size=vec2(1)]
|
|
6709
|
+
* @param {Color} [color=WHITE]
|
|
6710
|
+
* @param {number} [angle] */
|
|
6711
|
+
drawRect(pos, size, color, angle)
|
|
6712
|
+
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
6713
|
+
|
|
6600
6714
|
/** Clear a rectangle in layer space
|
|
6601
6715
|
* @param {Vector2} pos - position in pixel coordinates
|
|
6602
6716
|
* @param {Vector2} size
|
|
@@ -6675,7 +6789,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6675
6789
|
setCollisionData(layerPos, data=1)
|
|
6676
6790
|
{
|
|
6677
6791
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
6678
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
6792
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
6679
6793
|
layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
|
|
6680
6794
|
}
|
|
6681
6795
|
|
|
@@ -6690,7 +6804,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6690
6804
|
getCollisionData(layerPos)
|
|
6691
6805
|
{
|
|
6692
6806
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
6693
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
6807
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
6694
6808
|
return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
|
|
6695
6809
|
}
|
|
6696
6810
|
|
|
@@ -6715,8 +6829,10 @@ class TileCollisionLayer extends TileLayer
|
|
|
6715
6829
|
const posY = pos.y - this.pos.y;
|
|
6716
6830
|
const minX = max(posX - size.x/2|0, 0);
|
|
6717
6831
|
const minY = max(posY - size.y/2|0, 0);
|
|
6718
|
-
|
|
6719
|
-
|
|
6832
|
+
// ensure at least one cell is visited even when size is 0 and pos
|
|
6833
|
+
// lands exactly on an integer boundary (documented point-test mode)
|
|
6834
|
+
const maxX = min(max(posX + size.x/2, minX + 1), this.size.x);
|
|
6835
|
+
const maxY = min(max(posY + size.y/2, minY + 1), this.size.y);
|
|
6720
6836
|
const hitPos = new Vector2;
|
|
6721
6837
|
for (let y = minY; y < maxY; ++y)
|
|
6722
6838
|
for (let x = minX; x < maxX; ++x)
|
|
@@ -6835,7 +6951,7 @@ class ParticleEmitter extends EngineObject
|
|
|
6835
6951
|
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
6836
6952
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
6837
6953
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
6838
|
-
* @param {number} [fadeRate] -
|
|
6954
|
+
* @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
|
|
6839
6955
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
6840
6956
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
6841
6957
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
@@ -6919,7 +7035,7 @@ class ParticleEmitter extends EngineObject
|
|
|
6919
7035
|
this.gravityScale = gravityScale;
|
|
6920
7036
|
/** @property {number} - Cone for start particle angle */
|
|
6921
7037
|
this.particleConeAngle = particleConeAngle;
|
|
6922
|
-
/** @property {number} -
|
|
7038
|
+
/** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
|
|
6923
7039
|
this.fadeRate = fadeRate;
|
|
6924
7040
|
/** @property {number} - Apply extra randomness percent */
|
|
6925
7041
|
this.randomness = randomness;
|
|
@@ -7197,33 +7313,32 @@ class Particle
|
|
|
7197
7313
|
const hitLayer = tileCollisionTest(this.pos);
|
|
7198
7314
|
if (!testCollision(oldPos))
|
|
7199
7315
|
{
|
|
7200
|
-
|
|
7316
|
+
// testCollision already invoked collideCallback with the
|
|
7317
|
+
// correct (this, data, pos) args; no need to re-check here.
|
|
7318
|
+
// test which side we bounced off (or both if a corner)
|
|
7319
|
+
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
7320
|
+
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
7321
|
+
const hitRestitution = max(restitution, hitLayer.restitution);
|
|
7322
|
+
const hitFriction = max(friction, hitLayer.friction);
|
|
7323
|
+
if (isBlockedX)
|
|
7201
7324
|
{
|
|
7202
|
-
//
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
const hitFriction = max(friction, hitLayer.friction);
|
|
7207
|
-
if (isBlockedX)
|
|
7208
|
-
{
|
|
7209
|
-
// move to previous X position and bounce
|
|
7210
|
-
this.pos.x = oldPos.x;
|
|
7211
|
-
this.velocity.x *= -hitRestitution;
|
|
7212
|
-
this.velocity.y *= hitFriction;
|
|
7213
|
-
}
|
|
7214
|
-
if (isBlockedY || !isBlockedX)
|
|
7215
|
-
{
|
|
7216
|
-
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
7217
|
-
if (wasFalling)
|
|
7218
|
-
this.groundObject = hitLayer;
|
|
7219
|
-
|
|
7220
|
-
// move to previous Y position and bounce
|
|
7221
|
-
this.pos.y = oldPos.y;
|
|
7222
|
-
this.velocity.y *= -hitRestitution;
|
|
7223
|
-
this.velocity.x *= hitFriction;
|
|
7224
|
-
}
|
|
7225
|
-
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
7325
|
+
// move to previous X position and bounce
|
|
7326
|
+
this.pos.x = oldPos.x;
|
|
7327
|
+
this.velocity.x *= -hitRestitution;
|
|
7328
|
+
this.velocity.y *= hitFriction;
|
|
7226
7329
|
}
|
|
7330
|
+
if (isBlockedY || !isBlockedX)
|
|
7331
|
+
{
|
|
7332
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
7333
|
+
if (wasFalling)
|
|
7334
|
+
this.groundObject = hitLayer;
|
|
7335
|
+
|
|
7336
|
+
// move to previous Y position and bounce
|
|
7337
|
+
this.pos.y = oldPos.y;
|
|
7338
|
+
this.velocity.y *= -hitRestitution;
|
|
7339
|
+
this.velocity.x *= hitFriction;
|
|
7340
|
+
}
|
|
7341
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
7227
7342
|
}
|
|
7228
7343
|
}
|
|
7229
7344
|
}
|
|
@@ -7384,6 +7499,10 @@ function glInit(rootElement)
|
|
|
7384
7499
|
for (const info of glTextureInfos)
|
|
7385
7500
|
info.glTexture = undefined;
|
|
7386
7501
|
glActiveTexture = undefined;
|
|
7502
|
+
// drop any partially-filled batch so the next glFlush doesn't
|
|
7503
|
+
// upload stale glBatchCount against fresh empty buffers on restore
|
|
7504
|
+
glBatchCount = 0;
|
|
7505
|
+
glPolyMode = false;
|
|
7387
7506
|
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
7388
7507
|
});
|
|
7389
7508
|
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
@@ -7742,6 +7861,10 @@ function glSetTextureData(texture, image)
|
|
|
7742
7861
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
7743
7862
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
7744
7863
|
|
|
7864
|
+
// keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
|
|
7865
|
+
if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
|
|
7866
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
7867
|
+
|
|
7745
7868
|
// rebind active texture
|
|
7746
7869
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
7747
7870
|
}
|
|
@@ -7787,7 +7910,7 @@ function glFlush()
|
|
|
7787
7910
|
{
|
|
7788
7911
|
if (glEnable && glContext && glBatchCount)
|
|
7789
7912
|
{
|
|
7790
|
-
// set
|
|
7913
|
+
// set blend mode
|
|
7791
7914
|
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
7792
7915
|
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
7793
7916
|
glContext.enable(glContext.BLEND);
|
|
@@ -7864,9 +7987,9 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
7864
7987
|
}
|
|
7865
7988
|
|
|
7866
7989
|
/** Add an untextured rect to the gl draw list
|
|
7867
|
-
*
|
|
7868
|
-
*
|
|
7869
|
-
*
|
|
7990
|
+
* Zeroes the uvs and rgba so the texture contribution multiplies to 0,
|
|
7991
|
+
* then carries the real color in the additive slot. Works regardless of
|
|
7992
|
+
* which texture is currently bound.
|
|
7870
7993
|
* @param {number} x
|
|
7871
7994
|
* @param {number} y
|
|
7872
7995
|
* @param {number} sizeX
|
|
@@ -7876,52 +7999,7 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
7876
7999
|
* @memberof WebGL */
|
|
7877
8000
|
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
7878
8001
|
{
|
|
7879
|
-
|
|
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;
|
|
8002
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
7925
8003
|
}
|
|
7926
8004
|
|
|
7927
8005
|
/** Transform and add a polygon to the gl draw list
|
|
@@ -7937,13 +8015,13 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
|
7937
8015
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
7938
8016
|
{
|
|
7939
8017
|
const pointsOut = [];
|
|
8018
|
+
const sa = sin(-angle);
|
|
8019
|
+
const ca = cos(-angle);
|
|
7940
8020
|
for (const p of points)
|
|
7941
8021
|
{
|
|
7942
8022
|
// transform the point
|
|
7943
8023
|
const px = p.x*sx;
|
|
7944
8024
|
const py = p.y*sy;
|
|
7945
|
-
const sa = sin(-angle);
|
|
7946
|
-
const ca = cos(-angle);
|
|
7947
8025
|
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
7948
8026
|
}
|
|
7949
8027
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
@@ -7975,11 +8053,13 @@ function glDrawPoints(points, rgba)
|
|
|
7975
8053
|
{
|
|
7976
8054
|
if (!glEnable || points.length < 3)
|
|
7977
8055
|
return; // needs at least 3 points to have area
|
|
7978
|
-
|
|
8056
|
+
|
|
7979
8057
|
// flush if there is not enough room or if different blend mode
|
|
7980
8058
|
const vertCount = points.length + 2;
|
|
7981
8059
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
7982
8060
|
glFlush();
|
|
8061
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
8062
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
7983
8063
|
glSetPolyMode();
|
|
7984
8064
|
|
|
7985
8065
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -8003,11 +8083,13 @@ function glDrawColoredPoints(points, pointColors)
|
|
|
8003
8083
|
{
|
|
8004
8084
|
if (!glEnable || points.length < 3)
|
|
8005
8085
|
return; // needs at least 3 points to have area
|
|
8006
|
-
|
|
8086
|
+
|
|
8007
8087
|
// flush if there is not enough room or if different blend mode
|
|
8008
8088
|
const vertCount = points.length + 2;
|
|
8009
8089
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8010
8090
|
glFlush();
|
|
8091
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
8092
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
8011
8093
|
glSetPolyMode();
|
|
8012
8094
|
|
|
8013
8095
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -8077,7 +8159,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
8077
8159
|
const strip = [];
|
|
8078
8160
|
const n = points.length;
|
|
8079
8161
|
const e = 1e-6;
|
|
8080
|
-
|
|
8162
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
8163
|
+
const miterLimit = 10;
|
|
8081
8164
|
for (let i = 0; i < n; i++)
|
|
8082
8165
|
{
|
|
8083
8166
|
// for each vertex, calculate normal based on adjacent edges
|
|
@@ -8333,7 +8416,7 @@ function drawEngineLogo(t)
|
|
|
8333
8416
|
x.closePath();
|
|
8334
8417
|
gradient(0, Y, 0, Y+H,C);
|
|
8335
8418
|
}
|
|
8336
|
-
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}
|
|
8419
|
+
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%)`:'#000';
|
|
8337
8420
|
|
|
8338
8421
|
// center and fit to screen
|
|
8339
8422
|
const alpha = oscillate(1,1,t);
|
|
@@ -8743,8 +8826,20 @@ class NewgroundsPlugin
|
|
|
8743
8826
|
|
|
8744
8827
|
// get medals
|
|
8745
8828
|
const medalsResult = this.call('Medal.getList');
|
|
8829
|
+
|
|
8830
|
+
// bail early if the first call failed (offline / bad session /
|
|
8831
|
+
// server error) so we don't block the main thread on more sync
|
|
8832
|
+
// XHRs that are guaranteed to also fail
|
|
8833
|
+
if (!medalsResult || !medalsResult.result || medalsResult.result.error)
|
|
8834
|
+
{
|
|
8835
|
+
debugMedals && LOG('Newgrounds session unavailable; skipping plugin init');
|
|
8836
|
+
this.medals = [];
|
|
8837
|
+
this.scoreboards = [];
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8840
|
+
|
|
8746
8841
|
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
8747
|
-
this.medals = medalsResult
|
|
8842
|
+
this.medals = medalsResult.result.data?.['medals'] || [];
|
|
8748
8843
|
debugMedals && LOG(this.medals);
|
|
8749
8844
|
for (const newgroundsMedal of this.medals)
|
|
8750
8845
|
{
|
|
@@ -8764,11 +8859,11 @@ class NewgroundsPlugin
|
|
|
8764
8859
|
medal.description = medal.description + ` (${ medal.value })`;
|
|
8765
8860
|
}
|
|
8766
8861
|
}
|
|
8767
|
-
|
|
8862
|
+
|
|
8768
8863
|
// get scoreboards
|
|
8769
8864
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
8770
8865
|
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
8771
|
-
this.scoreboards = scoreboardResult
|
|
8866
|
+
this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
|
|
8772
8867
|
debugMedals && LOG(this.scoreboards);
|
|
8773
8868
|
|
|
8774
8869
|
// keep the session alive with a ping every minute
|
|
@@ -8952,10 +9047,14 @@ class PostProcessPlugin
|
|
|
8952
9047
|
function postProcessRender()
|
|
8953
9048
|
{
|
|
8954
9049
|
if (headlessMode || !glEnable) return;
|
|
8955
|
-
|
|
9050
|
+
|
|
8956
9051
|
// clear out the buffer
|
|
8957
9052
|
glFlush();
|
|
8958
9053
|
|
|
9054
|
+
// ensure we render to the default framebuffer (in case any earlier
|
|
9055
|
+
// caller this frame left a render target bound)
|
|
9056
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
9057
|
+
|
|
8959
9058
|
// setup shader program to draw a quad
|
|
8960
9059
|
glContext.useProgram(postProcess.shader);
|
|
8961
9060
|
glContext.bindVertexArray(postProcess.vao);
|
|
@@ -8996,6 +9095,9 @@ class PostProcessPlugin
|
|
|
8996
9095
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
|
|
8997
9096
|
}
|
|
8998
9097
|
|
|
9098
|
+
// restore default so subsequent dynamic texture uploads aren't flipped
|
|
9099
|
+
glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
|
|
9100
|
+
|
|
8999
9101
|
// force it to set instanced mode
|
|
9000
9102
|
glSetInstancedMode(true);
|
|
9001
9103
|
}
|
|
@@ -9622,11 +9724,17 @@ class UISystemPlugin
|
|
|
9622
9724
|
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
9623
9725
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
9624
9726
|
{
|
|
9625
|
-
|
|
9727
|
+
// remove any prior listeners so repeated setup calls don't stack
|
|
9728
|
+
if (this._dragListeners)
|
|
9729
|
+
for (const [type, listener] of this._dragListeners)
|
|
9730
|
+
document.removeEventListener(type, listener);
|
|
9731
|
+
this._dragListeners = [];
|
|
9732
|
+
const setCallback = (callback, listenerType)=>
|
|
9626
9733
|
{
|
|
9627
|
-
|
|
9734
|
+
const listener = (e)=> { e.preventDefault(); callback && callback(e); };
|
|
9628
9735
|
document.addEventListener(listenerType, listener);
|
|
9629
|
-
|
|
9736
|
+
this._dragListeners.push([listenerType, listener]);
|
|
9737
|
+
};
|
|
9630
9738
|
setCallback(onDrop, 'drop');
|
|
9631
9739
|
setCallback(onDragEnter, 'dragenter');
|
|
9632
9740
|
setCallback(onDragLeave, 'dragleave');
|
|
@@ -9950,6 +10058,15 @@ class UIObject
|
|
|
9950
10058
|
if (this.destroyed)
|
|
9951
10059
|
return;
|
|
9952
10060
|
|
|
10061
|
+
// clear ui-system references that point at this object so events
|
|
10062
|
+
// don't keep firing against a destroyed target (especially the
|
|
10063
|
+
// keydown listener attached for keyInputObject)
|
|
10064
|
+
if (uiSystem.activeObject === this) uiSystem.activeObject = undefined;
|
|
10065
|
+
if (uiSystem.hoverObject === this) uiSystem.hoverObject = undefined;
|
|
10066
|
+
if (uiSystem.lastHoverObject === this) uiSystem.lastHoverObject = undefined;
|
|
10067
|
+
if (uiSystem.navigationObject === this) uiSystem.navigationObject = undefined;
|
|
10068
|
+
if (uiSystem.keyInputObject === this) uiSystem.keyInputObject = undefined;
|
|
10069
|
+
|
|
9953
10070
|
// disconnect from parent and destroy children
|
|
9954
10071
|
this.destroyed = 1;
|
|
9955
10072
|
this.parent?.removeChild(this);
|
|
@@ -9958,6 +10075,8 @@ class UIObject
|
|
|
9958
10075
|
child.parent = undefined;
|
|
9959
10076
|
child.destroy();
|
|
9960
10077
|
}
|
|
10078
|
+
// clear references so destroyed children can be GC'd
|
|
10079
|
+
this.children.length = 0;
|
|
9961
10080
|
}
|
|
9962
10081
|
|
|
9963
10082
|
/** Check if the mouse is overlapping this ui object
|
|
@@ -10547,6 +10666,7 @@ class UISlider extends UIObject
|
|
|
10547
10666
|
{
|
|
10548
10667
|
// toggle value between 0 and 1
|
|
10549
10668
|
this.value = this.value ? 0 : 1;
|
|
10669
|
+
this.onChange();
|
|
10550
10670
|
this.onRelease();
|
|
10551
10671
|
super.navigatePressed();
|
|
10552
10672
|
}
|
|
@@ -10906,6 +11026,11 @@ class Box2dObject extends EngineObject
|
|
|
10906
11026
|
// destroy physics body, fixtures, and joints
|
|
10907
11027
|
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
10908
11028
|
box2d.world.DestroyBody(this.body);
|
|
11029
|
+
|
|
11030
|
+
// remove from tracked list so paused / headless sessions don't leak
|
|
11031
|
+
const i = box2d.objects.indexOf(this);
|
|
11032
|
+
if (i >= 0)
|
|
11033
|
+
box2d.objects.splice(i, 1);
|
|
10909
11034
|
super.destroy();
|
|
10910
11035
|
}
|
|
10911
11036
|
|
|
@@ -10994,7 +11119,9 @@ class Box2dObject extends EngineObject
|
|
|
10994
11119
|
/** Add a box shape to the body
|
|
10995
11120
|
* @param {Vector2} [size]
|
|
10996
11121
|
* @param {Vector2} [offset]
|
|
10997
|
-
* @param {number} [angle]
|
|
11122
|
+
* @param {number} [angle] - LittleJS convention (clockwise positive).
|
|
11123
|
+
* Negated internally to match Box2D's CCW-positive convention so the
|
|
11124
|
+
* fixture aligns with the same angle passed to drawRect/drawTile.
|
|
10998
11125
|
* @param {number} [density]
|
|
10999
11126
|
* @param {number} [friction]
|
|
11000
11127
|
* @param {number} [restitution]
|
|
@@ -11007,7 +11134,7 @@ class Box2dObject extends EngineObject
|
|
|
11007
11134
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
11008
11135
|
|
|
11009
11136
|
const shape = new box2d.instance.b2PolygonShape();
|
|
11010
|
-
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
11137
|
+
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), -angle);
|
|
11011
11138
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
11012
11139
|
}
|
|
11013
11140
|
|
|
@@ -11309,9 +11436,10 @@ class Box2dObject extends EngineObject
|
|
|
11309
11436
|
{
|
|
11310
11437
|
const data = new box2d.instance.b2MassData();
|
|
11311
11438
|
this.body.GetMassData(data);
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11439
|
+
// use !== undefined so setMass(0) (static-equivalent) isn't silently ignored
|
|
11440
|
+
if (localCenter !== undefined) data.set_center(box2d.vec2dTo(localCenter));
|
|
11441
|
+
if (mass !== undefined) data.set_mass(mass);
|
|
11442
|
+
if (momentOfInertia !== undefined) data.set_I(momentOfInertia);
|
|
11315
11443
|
this.body.SetMassData(data);
|
|
11316
11444
|
}
|
|
11317
11445
|
|
|
@@ -12483,6 +12611,8 @@ class Box2dPlugin
|
|
|
12483
12611
|
const fixtureB = contact.GetFixtureB();
|
|
12484
12612
|
const objectA = fixtureA.GetBody().object;
|
|
12485
12613
|
const objectB = fixtureB.GetBody().object;
|
|
12614
|
+
// raw user-created b2Bodies may have no .object — skip those
|
|
12615
|
+
if (!objectA || !objectB) return;
|
|
12486
12616
|
objectA.beginContact(objectB);
|
|
12487
12617
|
objectB.beginContact(objectA);
|
|
12488
12618
|
}
|
|
@@ -12493,6 +12623,7 @@ class Box2dPlugin
|
|
|
12493
12623
|
const fixtureB = contact.GetFixtureB();
|
|
12494
12624
|
const objectA = fixtureA.GetBody().object;
|
|
12495
12625
|
const objectB = fixtureB.GetBody().object;
|
|
12626
|
+
if (!objectA || !objectB) return;
|
|
12496
12627
|
objectA.endContact(objectB);
|
|
12497
12628
|
objectB.endContact(objectA);
|
|
12498
12629
|
};
|
|
@@ -12871,7 +13002,7 @@ async function box2dInit()
|
|
|
12871
13002
|
debugDraw.DrawTransform = function(transform)
|
|
12872
13003
|
{
|
|
12873
13004
|
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
|
|
12874
|
-
const pos =
|
|
13005
|
+
const pos = box2d.vec2From(transform.get_p());
|
|
12875
13006
|
const angle = -transform.get_q().GetAngle();
|
|
12876
13007
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
12877
13008
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
|
@@ -13275,7 +13406,7 @@ const Ease =
|
|
|
13275
13406
|
* @param {number} x
|
|
13276
13407
|
* @returns {number}
|
|
13277
13408
|
* @memberof TweenSystem */
|
|
13278
|
-
EXPO: (x) => 2 ** (10 * x - 10),
|
|
13409
|
+
EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
|
|
13279
13410
|
|
|
13280
13411
|
/** Back ease-in: overshoots backward at the start before snapping forward.
|
|
13281
13412
|
* @param {number} x
|
|
@@ -13288,6 +13419,8 @@ const Ease =
|
|
|
13288
13419
|
* @returns {number}
|
|
13289
13420
|
* @memberof TweenSystem */
|
|
13290
13421
|
ELASTIC: (x) =>
|
|
13422
|
+
x === 0 ? 0 :
|
|
13423
|
+
x === 1 ? 1 :
|
|
13291
13424
|
-(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
|
|
13292
13425
|
|
|
13293
13426
|
/** Spring-like ease-out: oscillates outward after passing the target.
|
|
@@ -13448,29 +13581,32 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
13448
13581
|
}
|
|
13449
13582
|
|
|
13450
13583
|
// Continuation that schedules the next loop iteration when one finishes.
|
|
13451
|
-
//
|
|
13452
|
-
//
|
|
13453
|
-
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
13460
|
-
|
|
13461
|
-
|
|
13462
|
-
|
|
13463
|
-
|
|
13464
|
-
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13472
|
-
|
|
13473
|
-
|
|
13584
|
+
// Reuses the same Tween object across iterations so the user's handle
|
|
13585
|
+
// from `.loop()` keeps working — calling `.stop()` mid-loop now cancels
|
|
13586
|
+
// the entire chain instead of just the current iteration.
|
|
13587
|
+
function loopContinuation(tween)
|
|
13588
|
+
{
|
|
13589
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
13590
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
13591
|
+
tween.life = tween.duration;
|
|
13592
|
+
tween.thenCallback = () => loopContinuation(tween);
|
|
13593
|
+
tweenActive.push(tween);
|
|
13594
|
+
// snap to start for the new iteration (matches Tween constructor behavior)
|
|
13595
|
+
tween.callback(tween.interp(tween.duration));
|
|
13596
|
+
}
|
|
13597
|
+
|
|
13598
|
+
// Continuation for pingPong: swaps start and end on the same tween each iteration.
|
|
13599
|
+
function pingPongContinuation(tween)
|
|
13600
|
+
{
|
|
13601
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
13602
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
13603
|
+
const tmp = tween.start;
|
|
13604
|
+
tween.start = tween.end;
|
|
13605
|
+
tween.end = tmp;
|
|
13606
|
+
tween.life = tween.duration;
|
|
13607
|
+
tween.thenCallback = () => pingPongContinuation(tween);
|
|
13608
|
+
tweenActive.push(tween);
|
|
13609
|
+
tween.callback(tween.interp(tween.duration));
|
|
13474
13610
|
}
|
|
13475
13611
|
|
|
13476
13612
|
/** Engine plugin hook: advance every active tween by the appropriate delta.
|
|
@@ -13804,11 +13940,13 @@ class PathFinder
|
|
|
13804
13940
|
if (dx !== 0 && dy !== 0)
|
|
13805
13941
|
{
|
|
13806
13942
|
// Diagonal step: refuse if either cardinal neighbor is
|
|
13807
|
-
// blocked
|
|
13943
|
+
// blocked. Prevents cutting through walls at corners.
|
|
13944
|
+
// (Costed-but-walkable cardinals do not block — diagonal
|
|
13945
|
+
// movement around expensive terrain is standard A*.)
|
|
13808
13946
|
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
13809
|
-
if (!card1 ||
|
|
13947
|
+
if (!card1 || !card1.walkable) continue;
|
|
13810
13948
|
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
13811
|
-
if (!card2 ||
|
|
13949
|
+
if (!card2 || !card2.walkable) continue;
|
|
13812
13950
|
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
13813
13951
|
}
|
|
13814
13952
|
|