littlejsengine 1.18.8 → 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 +30 -11
- package/dist/littlejs.d.ts +166 -74
- package/dist/littlejs.esm.js +610 -234
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +597 -229
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +590 -220
- package/package.json +4 -1
- package/plugins/box2d.js +17 -6
- package/plugins/medalSystem.js +2 -1
- package/plugins/newgrounds.js +22 -3
- package/plugins/pathFinder.js +40 -9
- package/plugins/pluginExport.js +4 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +33 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +16 -4
- package/src/engineAudio.js +30 -15
- package/src/engineBuild.mjs +1 -1
- package/src/engineDebug.js +7 -9
- package/src/engineDraw.js +144 -33
- package/src/engineExport.js +9 -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 +67 -11
- package/src/engineWebGL.js +37 -7
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);
|
|
@@ -207,7 +218,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
207
218
|
const combinedScale = timeScale * debugScale;
|
|
208
219
|
frameTimeDeltaMS *= combinedScale;
|
|
209
220
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
210
|
-
if (
|
|
221
|
+
if (combinedScale <= 1)
|
|
211
222
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
212
223
|
|
|
213
224
|
let wasUpdated = false;
|
|
@@ -294,6 +305,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
294
305
|
glFlush();
|
|
295
306
|
debugRenderPost();
|
|
296
307
|
drawCount = 0;
|
|
308
|
+
primitiveCount = 0;
|
|
297
309
|
}
|
|
298
310
|
}
|
|
299
311
|
|
|
@@ -424,7 +436,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
424
436
|
promises.push(loadTexture(0));
|
|
425
437
|
|
|
426
438
|
// load engine font image
|
|
427
|
-
promises.push(
|
|
439
|
+
promises.push(imageFontInit());
|
|
428
440
|
|
|
429
441
|
if (showSplashScreen)
|
|
430
442
|
{
|
|
@@ -670,19 +682,19 @@ const max = Math.max;
|
|
|
670
682
|
* @param {number} x
|
|
671
683
|
* @return {number}
|
|
672
684
|
* @memberof Math */
|
|
673
|
-
const sign = Math.sign;
|
|
685
|
+
const sign = (x) => Math.sign(x);
|
|
674
686
|
|
|
675
687
|
/** Returns hypotenuse of values passed in
|
|
676
688
|
* @param {...number} values
|
|
677
689
|
* @return {number}
|
|
678
690
|
* @memberof Math */
|
|
679
|
-
const hypot = Math.hypot;
|
|
691
|
+
const hypot = (...values) => Math.hypot(...values);
|
|
680
692
|
|
|
681
693
|
/** Returns log2 of value passed in
|
|
682
694
|
* @param {number} x
|
|
683
695
|
* @return {number}
|
|
684
696
|
* @memberof Math */
|
|
685
|
-
const log2 = Math.log2;
|
|
697
|
+
const log2 = (x) => Math.log2(x);
|
|
686
698
|
|
|
687
699
|
/** Returns sin of value passed in
|
|
688
700
|
* @param {number} x
|
|
@@ -824,7 +836,8 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
|
824
836
|
const dy = (posA.y - posB.y)*2;
|
|
825
837
|
const sx = sizeA.x + sizeB.x;
|
|
826
838
|
const sy = sizeA.y + sizeB.y;
|
|
827
|
-
|
|
839
|
+
// symmetric so isOverlapping(A,B) === isOverlapping(B,A) at touching edges
|
|
840
|
+
return abs(dx) < sx && abs(dy) < sy;
|
|
828
841
|
}
|
|
829
842
|
|
|
830
843
|
/** Returns true if a line segment is intersecting an axis aligned box
|
|
@@ -913,7 +926,7 @@ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'
|
|
|
913
926
|
/**
|
|
914
927
|
* Check if object is an array
|
|
915
928
|
* @param {any} a
|
|
916
|
-
* @return {
|
|
929
|
+
* @return {a is Array<any>}
|
|
917
930
|
* @memberof Math */
|
|
918
931
|
function isArray(a) { return Array.isArray(a); }
|
|
919
932
|
|
|
@@ -1051,7 +1064,13 @@ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length);
|
|
|
1051
1064
|
* @return {Vector2}
|
|
1052
1065
|
* @memberof Random */
|
|
1053
1066
|
function randInCircle(radius=1, minRadius=0)
|
|
1054
|
-
{
|
|
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
|
+
}
|
|
1055
1074
|
|
|
1056
1075
|
/** Returns a random color between the two passed in colors, combine components if linear
|
|
1057
1076
|
* @param {Color} [colorA=WHITE]
|
|
@@ -1121,7 +1140,14 @@ class RandomGenerator
|
|
|
1121
1140
|
* @param {number} [valueA]
|
|
1122
1141
|
* @param {number} [valueB]
|
|
1123
1142
|
* @return {number} */
|
|
1124
|
-
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
|
+
}
|
|
1125
1151
|
|
|
1126
1152
|
/** Returns a random angle between -PI and PI
|
|
1127
1153
|
* @return {number} */
|
|
@@ -1769,6 +1795,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
1769
1795
|
* - File saving (text, canvas, data URLs)
|
|
1770
1796
|
* - Native share dialog support
|
|
1771
1797
|
* - Local storage save data management
|
|
1798
|
+
* - Gradient noise (1D and 2D)
|
|
1772
1799
|
* @namespace Utilities
|
|
1773
1800
|
*/
|
|
1774
1801
|
|
|
@@ -1833,9 +1860,15 @@ class Timer
|
|
|
1833
1860
|
* @return {number} */
|
|
1834
1861
|
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
1835
1862
|
|
|
1836
|
-
/** 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).
|
|
1837
1865
|
* @return {number} */
|
|
1838
|
-
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
|
+
}
|
|
1839
1872
|
|
|
1840
1873
|
/** Get the time this timer was set to, returns 0 if not set
|
|
1841
1874
|
* @return {number} */
|
|
@@ -1862,9 +1895,9 @@ class Timer
|
|
|
1862
1895
|
* @memberof Utilities */
|
|
1863
1896
|
function formatTime(t)
|
|
1864
1897
|
{
|
|
1865
|
-
const
|
|
1898
|
+
const signStr = t < 0 ? '-' : '';
|
|
1866
1899
|
t = abs(t)|0;
|
|
1867
|
-
return
|
|
1900
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
1868
1901
|
}
|
|
1869
1902
|
|
|
1870
1903
|
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
@@ -1950,15 +1983,20 @@ function shareURL(title, url, callback)
|
|
|
1950
1983
|
function readSaveData(saveName, defaultSaveData)
|
|
1951
1984
|
{
|
|
1952
1985
|
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
1953
|
-
|
|
1954
|
-
//
|
|
1955
|
-
|
|
1986
|
+
|
|
1987
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
1988
|
+
// iframes) and corrupt JSON in stored data
|
|
1956
1989
|
let loadedData = {};
|
|
1957
|
-
|
|
1990
|
+
try
|
|
1958
1991
|
{
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
+
}
|
|
1961
1998
|
}
|
|
1999
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
1962
2000
|
return { ...defaultSaveData, ...loadedData };
|
|
1963
2001
|
}
|
|
1964
2002
|
|
|
@@ -1969,7 +2007,51 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
1969
2007
|
function writeSaveData(saveName, saveData)
|
|
1970
2008
|
{
|
|
1971
2009
|
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
1972
|
-
localStorage
|
|
2010
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
2011
|
+
try { localStorage[saveName] = JSON.stringify(saveData); }
|
|
2012
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2016
|
+
|
|
2017
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
2018
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
2019
|
+
function noiseHash(i)
|
|
2020
|
+
{
|
|
2021
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
2022
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
2023
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
2024
|
+
h ^= h >>> 16;
|
|
2025
|
+
return (h >>> 0) / 2**32;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
|
|
2029
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
2030
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
2031
|
+
* @param {number} x
|
|
2032
|
+
* @return {number}
|
|
2033
|
+
* @memberof Utilities */
|
|
2034
|
+
function noise1D(x)
|
|
2035
|
+
{
|
|
2036
|
+
const i = floor(x);
|
|
2037
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
2041
|
+
* @param {number} x
|
|
2042
|
+
* @param {number} y
|
|
2043
|
+
* @return {number}
|
|
2044
|
+
* @memberof Utilities */
|
|
2045
|
+
function noise2D(x, y)
|
|
2046
|
+
{
|
|
2047
|
+
const ix = floor(x), iy = floor(y);
|
|
2048
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
2049
|
+
// large prime decorrelates neighboring rows
|
|
2050
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
2051
|
+
return lerp(
|
|
2052
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
2053
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
2054
|
+
fy);
|
|
1973
2055
|
}
|
|
1974
2056
|
/**
|
|
1975
2057
|
* LittleJS Engine Settings
|
|
@@ -2022,7 +2104,7 @@ let canvasColorTiles = true;
|
|
|
2022
2104
|
|
|
2023
2105
|
/** Color to clear the canvas to before render, does not clear if alpha is 0
|
|
2024
2106
|
* @type {Color}
|
|
2025
|
-
* @memberof
|
|
2107
|
+
* @memberof Settings */
|
|
2026
2108
|
let canvasClearColor = CLEAR_BLACK;
|
|
2027
2109
|
|
|
2028
2110
|
/** The max size of the canvas, centered if window is larger
|
|
@@ -2222,7 +2304,8 @@ let touchInputEnable = true;
|
|
|
2222
2304
|
let touchGamepadEnable = false;
|
|
2223
2305
|
|
|
2224
2306
|
/** True if touch gamepad should have start button in the center
|
|
2225
|
-
* - 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)
|
|
2226
2309
|
* - When the game is paused, any touch will press the button
|
|
2227
2310
|
* - Set size to enable the center button
|
|
2228
2311
|
* @type {number}
|
|
@@ -2634,7 +2717,7 @@ class EngineObject
|
|
|
2634
2717
|
this.color = color.copy();
|
|
2635
2718
|
/** @property {Color} - Additive color to apply when rendered */
|
|
2636
2719
|
this.additiveColor = undefined;
|
|
2637
|
-
/** @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. */
|
|
2638
2721
|
this.mirror = false;
|
|
2639
2722
|
/** @property {boolean} - Has object been destroyed? */
|
|
2640
2723
|
this.destroyed = false;
|
|
@@ -2720,6 +2803,9 @@ class EngineObject
|
|
|
2720
2803
|
// child objects do not have physics
|
|
2721
2804
|
ASSERT(!this.parent);
|
|
2722
2805
|
|
|
2806
|
+
// bail if a collision callback destroyed us mid-frame
|
|
2807
|
+
if (this.destroyed) return;
|
|
2808
|
+
|
|
2723
2809
|
if (this.clampSpeed)
|
|
2724
2810
|
{
|
|
2725
2811
|
// limit max speed to prevent missing collisions
|
|
@@ -2775,6 +2861,8 @@ class EngineObject
|
|
|
2775
2861
|
|
|
2776
2862
|
// notify objects of collision and check if should be resolved
|
|
2777
2863
|
const collide1 = this.collideWithObject(o);
|
|
2864
|
+
// callback may have destroyed us; stop resolving against more objects
|
|
2865
|
+
if (this.destroyed) return;
|
|
2778
2866
|
const collide2 = o.collideWithObject(this);
|
|
2779
2867
|
if (!collide1 || !collide2) continue;
|
|
2780
2868
|
|
|
@@ -2870,13 +2958,17 @@ class EngineObject
|
|
|
2870
2958
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
2871
2959
|
if (isBlockedX)
|
|
2872
2960
|
{
|
|
2873
|
-
// 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)
|
|
2874
2964
|
const epsilon = 1e-3;
|
|
2875
|
-
const
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
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)
|
|
2880
2972
|
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
2881
2973
|
{
|
|
2882
2974
|
this.pos.y = y;
|
|
@@ -3016,6 +3108,8 @@ class EngineObject
|
|
|
3016
3108
|
* @return {EngineObject} The child object added */
|
|
3017
3109
|
addChild(child, localPos=vec2(), localAngle=0)
|
|
3018
3110
|
{
|
|
3111
|
+
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
3112
|
+
if (this.destroyed) return child;
|
|
3019
3113
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
3020
3114
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3021
3115
|
ASSERT(child !== this, 'cannot add self as child');
|
|
@@ -3032,10 +3126,7 @@ class EngineObject
|
|
|
3032
3126
|
removeChild(child)
|
|
3033
3127
|
{
|
|
3034
3128
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
3035
|
-
|
|
3036
|
-
const index = this.children.indexOf(child);
|
|
3037
|
-
ASSERT(index >= 0, 'child not found in children array');
|
|
3038
|
-
index >= 0 && this.children.splice(index, 1);
|
|
3129
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
3039
3130
|
child.parent = undefined;
|
|
3040
3131
|
}
|
|
3041
3132
|
|
|
@@ -3110,7 +3201,7 @@ class EngineObject
|
|
|
3110
3201
|
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
3111
3202
|
* - Primitive drawing for polygons, ellipses, and lines
|
|
3112
3203
|
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
3113
|
-
* - Text rendering with custom fonts and
|
|
3204
|
+
* - Text rendering with custom fonts and ImageFont support
|
|
3114
3205
|
* - Color and additive color blending for effects
|
|
3115
3206
|
* - Rotation, mirroring, and scaling transformations
|
|
3116
3207
|
* - Camera system with position, scale, and rotation
|
|
@@ -3176,6 +3267,12 @@ let textureInfos = [];
|
|
|
3176
3267
|
* @memberof Draw */
|
|
3177
3268
|
let drawCount;
|
|
3178
3269
|
|
|
3270
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
3271
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
3272
|
+
* @type {number}
|
|
3273
|
+
* @memberof Draw */
|
|
3274
|
+
let primitiveCount;
|
|
3275
|
+
|
|
3179
3276
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
3180
3277
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
3181
3278
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
@@ -3200,7 +3297,7 @@ let drawCount;
|
|
|
3200
3297
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
3201
3298
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
3202
3299
|
* @memberof Draw */
|
|
3203
|
-
function tile(index=
|
|
3300
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3204
3301
|
{
|
|
3205
3302
|
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
3206
3303
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
@@ -3251,8 +3348,8 @@ class TileInfo
|
|
|
3251
3348
|
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
3252
3349
|
* @param {Vector2} [size] - Size of tile in pixels
|
|
3253
3350
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
3254
|
-
* @param {number} [padding] - How many pixels padding around
|
|
3255
|
-
* @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)
|
|
3256
3353
|
*/
|
|
3257
3354
|
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3258
3355
|
{
|
|
@@ -3284,7 +3381,7 @@ class TileInfo
|
|
|
3284
3381
|
ASSERT(typeof frame === 'number');
|
|
3285
3382
|
const w = this.size.x + this.padding*2;
|
|
3286
3383
|
const x = frame*w;
|
|
3287
|
-
ASSERT(x
|
|
3384
|
+
ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
3288
3385
|
return this.offset(new Vector2(x));
|
|
3289
3386
|
}
|
|
3290
3387
|
|
|
@@ -3373,7 +3470,7 @@ class TextureInfo
|
|
|
3373
3470
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
3374
3471
|
* @memberof Draw */
|
|
3375
3472
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
3376
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
3473
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
3377
3474
|
{
|
|
3378
3475
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3379
3476
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -3416,19 +3513,17 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
3416
3513
|
}
|
|
3417
3514
|
else
|
|
3418
3515
|
{
|
|
3419
|
-
//
|
|
3420
|
-
// texture is bound doesn't leak in) and folding color+additive
|
|
3421
|
-
// into the additive slot — matches the Canvas2D path's
|
|
3516
|
+
// untextured: fold color+additive to match the Canvas2D path's
|
|
3422
3517
|
// color.add(additiveColor) on line ~337.
|
|
3423
3518
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
3424
|
-
|
|
3425
|
-
0, combined.rgbaInt());
|
|
3519
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
3426
3520
|
}
|
|
3427
3521
|
}
|
|
3428
3522
|
else
|
|
3429
3523
|
{
|
|
3430
3524
|
// normal canvas 2D rendering method (slower)
|
|
3431
3525
|
++drawCount;
|
|
3526
|
+
++primitiveCount;
|
|
3432
3527
|
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
3433
3528
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
3434
3529
|
{
|
|
@@ -3468,13 +3563,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
3468
3563
|
* @param {Vector2} pos
|
|
3469
3564
|
* @param {Vector2} [size=vec2(1)]
|
|
3470
3565
|
* @param {Color} [colorTop=WHITE]
|
|
3471
|
-
* @param {Color} [colorBottom=
|
|
3566
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
3472
3567
|
* @param {number} [angle]
|
|
3473
3568
|
* @param {boolean} [useWebGL=glEnable]
|
|
3474
3569
|
* @param {boolean} [screenSpace]
|
|
3475
3570
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3476
3571
|
* @memberof Draw */
|
|
3477
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=
|
|
3572
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3478
3573
|
{
|
|
3479
3574
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3480
3575
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -3514,6 +3609,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3514
3609
|
{
|
|
3515
3610
|
// normal canvas 2D rendering method (slower)
|
|
3516
3611
|
++drawCount;
|
|
3612
|
+
++primitiveCount;
|
|
3517
3613
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
3518
3614
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
3519
3615
|
{
|
|
@@ -3576,8 +3672,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
3576
3672
|
return;
|
|
3577
3673
|
}
|
|
3578
3674
|
|
|
3579
|
-
// Canvas2D path — increment
|
|
3675
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
3580
3676
|
++drawCount;
|
|
3677
|
+
++primitiveCount;
|
|
3581
3678
|
|
|
3582
3679
|
if (!screenSpace)
|
|
3583
3680
|
{
|
|
@@ -3630,7 +3727,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
3630
3727
|
* @param {boolean} [screenSpace]
|
|
3631
3728
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3632
3729
|
* @memberof Draw */
|
|
3633
|
-
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)
|
|
3634
3731
|
{
|
|
3635
3732
|
ASSERT(isArray(points), 'points must be an array');
|
|
3636
3733
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -3651,6 +3748,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
3651
3748
|
{
|
|
3652
3749
|
// normal canvas 2D rendering method (slower)
|
|
3653
3750
|
++drawCount;
|
|
3751
|
+
++primitiveCount;
|
|
3654
3752
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
3655
3753
|
{
|
|
3656
3754
|
context.strokeStyle = color.toString();
|
|
@@ -3678,7 +3776,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
3678
3776
|
* @param {boolean} [screenSpace]
|
|
3679
3777
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3680
3778
|
* @memberof Draw */
|
|
3681
|
-
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)
|
|
3682
3780
|
{
|
|
3683
3781
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
3684
3782
|
const size = vec2(width, halfDelta.length()*2);
|
|
@@ -3694,9 +3792,9 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
3694
3792
|
* @param {Vector2} [size=vec2(1)]
|
|
3695
3793
|
* @param {number} [sides]
|
|
3696
3794
|
* @param {Color} [color=WHITE]
|
|
3697
|
-
* @param {number} [angle]
|
|
3698
3795
|
* @param {number} [lineWidth]
|
|
3699
3796
|
* @param {Color} [lineColor=BLACK]
|
|
3797
|
+
* @param {number} [angle]
|
|
3700
3798
|
* @param {boolean} [useWebGL=glEnable]
|
|
3701
3799
|
* @param {boolean} [screenSpace]
|
|
3702
3800
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -3789,7 +3887,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
3789
3887
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3790
3888
|
|
|
3791
3889
|
// clamp line width to prevent artifacts
|
|
3792
|
-
lineWidth = clamp(lineWidth, 0,
|
|
3890
|
+
lineWidth = clamp(lineWidth, 0, min(size.x, size.y));
|
|
3793
3891
|
|
|
3794
3892
|
if (useWebGL && glEnable)
|
|
3795
3893
|
{
|
|
@@ -3831,6 +3929,104 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
3831
3929
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
3832
3930
|
}
|
|
3833
3931
|
|
|
3932
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
3933
|
+
* - Best when batched with other untextured polys
|
|
3934
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
3935
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
3936
|
+
* @param {Vector2} pos
|
|
3937
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
3938
|
+
* @param {Color} [colorInner=WHITE]
|
|
3939
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
3940
|
+
* @param {number} [angle]
|
|
3941
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
3942
|
+
* @param {boolean} [screenSpace]
|
|
3943
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
3944
|
+
* @memberof Draw */
|
|
3945
|
+
let drawEllipseGradientOffset = 0;
|
|
3946
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
3947
|
+
{
|
|
3948
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3949
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
3950
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
3951
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3952
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3953
|
+
|
|
3954
|
+
if (headlessMode) return;
|
|
3955
|
+
|
|
3956
|
+
if (useWebGL && glEnable)
|
|
3957
|
+
{
|
|
3958
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3959
|
+
if (screenSpace)
|
|
3960
|
+
{
|
|
3961
|
+
// convert to world space
|
|
3962
|
+
pos = screenToWorld(pos);
|
|
3963
|
+
size = size.scale(1/cameraScale);
|
|
3964
|
+
angle += cameraAngle;
|
|
3965
|
+
}
|
|
3966
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
3967
|
+
// so back-to-back gradients at the same position have their hole
|
|
3968
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
3969
|
+
// verts and don't visibly stack
|
|
3970
|
+
const sides = glCircleSides;
|
|
3971
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
3972
|
+
const innerInt = colorInner.rgbaInt();
|
|
3973
|
+
const outerInt = colorOuter.rgbaInt();
|
|
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
|
+
};
|
|
3981
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
3982
|
+
const points = [rim(startA)];
|
|
3983
|
+
const colors = [outerInt];
|
|
3984
|
+
for (let i=sides; i--;)
|
|
3985
|
+
{
|
|
3986
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
3987
|
+
points.push(pos);
|
|
3988
|
+
colors.push(innerInt);
|
|
3989
|
+
points.push(rim(a));
|
|
3990
|
+
colors.push(outerInt);
|
|
3991
|
+
}
|
|
3992
|
+
glDrawColoredPoints(points, colors);
|
|
3993
|
+
}
|
|
3994
|
+
else
|
|
3995
|
+
{
|
|
3996
|
+
// normal canvas 2D rendering method (slower)
|
|
3997
|
+
++drawCount;
|
|
3998
|
+
++primitiveCount;
|
|
3999
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
4000
|
+
{
|
|
4001
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
4002
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
4003
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
4004
|
+
context.fillStyle = gradient;
|
|
4005
|
+
context.beginPath();
|
|
4006
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
4007
|
+
context.fill();
|
|
4008
|
+
}, screenSpace, context);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
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
|
+
|
|
3834
4030
|
/**
|
|
3835
4031
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
3836
4032
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -3885,7 +4081,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3885
4081
|
* @param {number} [angle]
|
|
3886
4082
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3887
4083
|
* @memberof Draw */
|
|
3888
|
-
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)
|
|
3889
4085
|
{
|
|
3890
4086
|
// convert to screen space
|
|
3891
4087
|
pos = worldToScreen(pos);
|
|
@@ -3925,16 +4121,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
3925
4121
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
3926
4122
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3927
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();
|
|
3928
4128
|
context.fillStyle = color.toString();
|
|
3929
4129
|
context.strokeStyle = lineColor.toString();
|
|
3930
4130
|
context.lineWidth = lineWidth;
|
|
3931
4131
|
context.textAlign = textAlign;
|
|
3932
4132
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
3933
4133
|
context.textBaseline = 'middle';
|
|
3934
|
-
|
|
3935
|
-
const lines = (text+'').split('\n');
|
|
3936
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
3937
|
-
context.save();
|
|
3938
4134
|
context.translate(pos.x, posY);
|
|
3939
4135
|
context.rotate(-angle);
|
|
3940
4136
|
let yOffset = 0;
|
|
@@ -4095,6 +4291,9 @@ function isOnScreen(pos, size=0)
|
|
|
4095
4291
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4096
4292
|
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
4097
4293
|
|
|
4294
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
4295
|
+
if (!cameraScale) return false;
|
|
4296
|
+
|
|
4098
4297
|
// optimized circle on screen test
|
|
4099
4298
|
// pos = worldToScreen(pos);
|
|
4100
4299
|
let x = pos.x - cameraPos.x;
|
|
@@ -4136,7 +4335,10 @@ function combineCanvases()
|
|
|
4136
4335
|
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
4137
4336
|
workCanvas.width = w;
|
|
4138
4337
|
workCanvas.height = h;
|
|
4139
|
-
|
|
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);
|
|
4140
4342
|
glCopyToContext(workContext);
|
|
4141
4343
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
4142
4344
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -4279,33 +4481,33 @@ function setCursor(cursorStyle = 'auto')
|
|
|
4279
4481
|
///////////////////////////////////////////////////////////////////////////////
|
|
4280
4482
|
|
|
4281
4483
|
/** Engine font image, 8x8 font provided by the engine
|
|
4282
|
-
* @type {
|
|
4484
|
+
* @type {ImageFont}
|
|
4283
4485
|
* @memberof Draw */
|
|
4284
|
-
let
|
|
4486
|
+
let engineImageFont;
|
|
4285
4487
|
|
|
4286
4488
|
/**
|
|
4287
|
-
* Font
|
|
4489
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
4288
4490
|
* - 96 characters (from space to tilde) are stored in an image
|
|
4289
4491
|
* - A 8x8 default engine font is supplied for general use
|
|
4290
4492
|
* - This system is WebGL enabled for fast text rendering
|
|
4291
4493
|
* - Fonts can also be colored and scaled along each axis
|
|
4292
|
-
*
|
|
4494
|
+
*
|
|
4293
4495
|
* @memberof Draw
|
|
4294
4496
|
* @example
|
|
4295
4497
|
* // use built in font
|
|
4296
|
-
* const font =
|
|
4498
|
+
* const font = engineImageFont;
|
|
4297
4499
|
*
|
|
4298
4500
|
* // draw text
|
|
4299
4501
|
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
4300
4502
|
*/
|
|
4301
|
-
class
|
|
4503
|
+
class ImageFont
|
|
4302
4504
|
{
|
|
4303
4505
|
/** Create an image font
|
|
4304
4506
|
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
4305
4507
|
*/
|
|
4306
4508
|
constructor(tileInfo)
|
|
4307
4509
|
{
|
|
4308
|
-
ASSERT(!!tileInfo, 'tileInfo is required for
|
|
4510
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
4309
4511
|
|
|
4310
4512
|
/** @property {TileInfo} - Tile info for the font */
|
|
4311
4513
|
this.tileInfo = tileInfo.frame(0);
|
|
@@ -4390,7 +4592,7 @@ class FontImage
|
|
|
4390
4592
|
}
|
|
4391
4593
|
|
|
4392
4594
|
// load engine font, called automatically on startup
|
|
4393
|
-
async function
|
|
4595
|
+
async function imageFontInit()
|
|
4394
4596
|
{
|
|
4395
4597
|
const image = new Image;
|
|
4396
4598
|
await new Promise(resolve =>
|
|
@@ -4403,7 +4605,7 @@ async function fontImageInit()
|
|
|
4403
4605
|
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
4404
4606
|
const textureInfo = new TextureInfo(image);
|
|
4405
4607
|
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
4406
|
-
|
|
4608
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
4407
4609
|
}
|
|
4408
4610
|
/**
|
|
4409
4611
|
* LittleJS Input System
|
|
@@ -4852,9 +5054,11 @@ function inputInit()
|
|
|
4852
5054
|
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
4853
5055
|
}
|
|
4854
5056
|
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
4855
|
-
function onMouseWheel(e)
|
|
4856
|
-
{
|
|
4857
|
-
|
|
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);
|
|
4858
5062
|
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
4859
5063
|
e.preventDefault(); // prevent page scrolling
|
|
4860
5064
|
}
|
|
@@ -4979,9 +5183,14 @@ function inputInit()
|
|
|
4979
5183
|
if (button < touchGamepadButtonCount)
|
|
4980
5184
|
touchGamepadButtons[button] = 1;
|
|
4981
5185
|
}
|
|
4982
|
-
else if (startCenter.distance(touchPos)
|
|
5186
|
+
else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize &&
|
|
5187
|
+
stickCenter.distance(touchPos) >= 2 * touchGamepadSize &&
|
|
5188
|
+
buttonCenter.distance(touchPos) >= 2 * touchGamepadSize)
|
|
4983
5189
|
{
|
|
4984
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
|
|
4985
5194
|
touchGamepadButtons[9] = 1;
|
|
4986
5195
|
}
|
|
4987
5196
|
}
|
|
@@ -5023,7 +5232,7 @@ function inputUpdate()
|
|
|
5023
5232
|
v > min ? percent(v, min, max) :
|
|
5024
5233
|
v < -min ? -percent(-v, min, max) : 0;
|
|
5025
5234
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5026
|
-
}
|
|
5235
|
+
};
|
|
5027
5236
|
|
|
5028
5237
|
// update touch gamepad if enabled
|
|
5029
5238
|
if (touchGamepadEnable && isTouchDevice)
|
|
@@ -5037,7 +5246,12 @@ function inputUpdate()
|
|
|
5037
5246
|
debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5038
5247
|
debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5039
5248
|
if (touchGamepadCenterButtonSize)
|
|
5249
|
+
{
|
|
5040
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
|
+
}
|
|
5041
5255
|
}
|
|
5042
5256
|
|
|
5043
5257
|
if (!touchGamepadTimer.isSet()) return;
|
|
@@ -5144,13 +5358,6 @@ function inputUpdate()
|
|
|
5144
5358
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5145
5359
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5146
5360
|
}
|
|
5147
|
-
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
5148
|
-
{
|
|
5149
|
-
// digital style dpad from axes
|
|
5150
|
-
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
5151
|
-
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
5152
|
-
dpad.set(x, -y);
|
|
5153
|
-
}
|
|
5154
5361
|
|
|
5155
5362
|
// copy dpad to left analog stick when pressed
|
|
5156
5363
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
@@ -5356,7 +5563,7 @@ class Sound
|
|
|
5356
5563
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
5357
5564
|
this.onloadCallback = onloadCallback;
|
|
5358
5565
|
|
|
5359
|
-
if (
|
|
5566
|
+
if (isArray(asset))
|
|
5360
5567
|
{
|
|
5361
5568
|
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
5362
5569
|
const zzfxSound = asset.slice();
|
|
@@ -5448,7 +5655,7 @@ class Sound
|
|
|
5448
5655
|
}
|
|
5449
5656
|
|
|
5450
5657
|
/** Get how long this sound is in seconds
|
|
5451
|
-
* @return {number} - How long the sound is in seconds (
|
|
5658
|
+
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
5452
5659
|
*/
|
|
5453
5660
|
getDuration()
|
|
5454
5661
|
{ return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
|
|
@@ -5605,10 +5812,14 @@ class SoundInstance
|
|
|
5605
5812
|
{
|
|
5606
5813
|
if (fadeTime)
|
|
5607
5814
|
{
|
|
5608
|
-
// 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
|
|
5609
5819
|
const startFade = audioContext.currentTime;
|
|
5610
5820
|
const endFade = startFade + fadeTime;
|
|
5611
|
-
this.gainNode.gain.
|
|
5821
|
+
this.gainNode.gain.cancelScheduledValues(startFade);
|
|
5822
|
+
this.gainNode.gain.setValueAtTime(this.volume, startFade);
|
|
5612
5823
|
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
5613
5824
|
this.source.stop(endFade);
|
|
5614
5825
|
}
|
|
@@ -5656,13 +5867,14 @@ class SoundInstance
|
|
|
5656
5867
|
*/
|
|
5657
5868
|
getCurrentTime()
|
|
5658
5869
|
{
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
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;
|
|
5662
5874
|
}
|
|
5663
5875
|
|
|
5664
5876
|
/** Get the total duration of this sound
|
|
5665
|
-
* @return {number} - Total duration in seconds
|
|
5877
|
+
* @return {number} - Total duration in seconds (0 if loading)
|
|
5666
5878
|
*/
|
|
5667
5879
|
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
5668
5880
|
|
|
@@ -5676,16 +5888,17 @@ class SoundInstance
|
|
|
5676
5888
|
|
|
5677
5889
|
/** Speak text with passed in settings
|
|
5678
5890
|
* @param {string} text - The text to speak
|
|
5679
|
-
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
5680
5891
|
* @param {number} [volume] - How much to scale volume by
|
|
5681
5892
|
* @param {number} [rate] - How quickly to speak
|
|
5682
5893
|
* @param {number} [pitch] - How much to change the pitch by
|
|
5894
|
+
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
5683
5895
|
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
5684
5896
|
* @memberof Audio */
|
|
5685
|
-
function speak(text,
|
|
5897
|
+
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
5686
5898
|
{
|
|
5899
|
+
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
5687
5900
|
if (!soundEnable || headlessMode) return;
|
|
5688
|
-
if (
|
|
5901
|
+
if (typeof speechSynthesis === 'undefined') return;
|
|
5689
5902
|
|
|
5690
5903
|
// common languages (not supported by all browsers)
|
|
5691
5904
|
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
@@ -5703,7 +5916,11 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
|
|
|
5703
5916
|
|
|
5704
5917
|
/** Stop all queued speech
|
|
5705
5918
|
* @memberof Audio */
|
|
5706
|
-
function speakStop()
|
|
5919
|
+
function speakStop()
|
|
5920
|
+
{
|
|
5921
|
+
if (typeof speechSynthesis !== 'undefined')
|
|
5922
|
+
speechSynthesis.cancel();
|
|
5923
|
+
}
|
|
5707
5924
|
|
|
5708
5925
|
/** Get frequency of a note on a musical scale
|
|
5709
5926
|
* @param {number} semitoneOffset - How many semitones away from the root note
|
|
@@ -5765,9 +5982,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
5765
5982
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
5766
5983
|
source.connect(pannerNode).connect(gainNode);
|
|
5767
5984
|
|
|
5768
|
-
//
|
|
5769
|
-
|
|
5770
|
-
|
|
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
|
+
});
|
|
5771
5993
|
|
|
5772
5994
|
// play and return sound
|
|
5773
5995
|
const startOffset = offset * rate;
|
|
@@ -6004,14 +6226,29 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
|
6004
6226
|
* @memberof TileLayers */
|
|
6005
6227
|
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
6006
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();
|
|
6007
6233
|
for (const layer of tileCollisionLayers)
|
|
6008
6234
|
{
|
|
6009
6235
|
if (!solidOnly || layer.isSolid)
|
|
6010
6236
|
{
|
|
6011
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject,
|
|
6012
|
-
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
|
+
}
|
|
6013
6248
|
}
|
|
6014
6249
|
}
|
|
6250
|
+
if (closestHit && normal) normal.setFrom(closestNormal);
|
|
6251
|
+
return closestHit;
|
|
6015
6252
|
}
|
|
6016
6253
|
|
|
6017
6254
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6185,38 +6422,6 @@ class CanvasLayer extends EngineObject
|
|
|
6185
6422
|
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
6186
6423
|
}
|
|
6187
6424
|
|
|
6188
|
-
/** Draw a tile onto the layer canvas in world space
|
|
6189
|
-
* @param {Vector2} pos
|
|
6190
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6191
|
-
* @param {TileInfo} [tileInfo]
|
|
6192
|
-
* @param {Color} [color=WHITE]
|
|
6193
|
-
* @param {number} [angle]
|
|
6194
|
-
* @param {boolean} [mirror] */
|
|
6195
|
-
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
6196
|
-
{
|
|
6197
|
-
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
6198
|
-
size = size.multiply(this.tileInfo.size);
|
|
6199
|
-
pos.y = this.canvas.height - pos.y;
|
|
6200
|
-
|
|
6201
|
-
// draw the tile onto the layer canvas
|
|
6202
|
-
const oldMainCanvasSize = mainCanvasSize;
|
|
6203
|
-
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
6204
|
-
const useWebGL = this.hasWebGL();
|
|
6205
|
-
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
6206
|
-
const drawContext = useWebGL ? undefined : this.context;
|
|
6207
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
6208
|
-
useWebGL && glSetRenderTarget();
|
|
6209
|
-
mainCanvasSize = oldMainCanvasSize;
|
|
6210
|
-
}
|
|
6211
|
-
|
|
6212
|
-
/** Draw a rectangle onto the layer canvas in world space
|
|
6213
|
-
* @param {Vector2} pos
|
|
6214
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6215
|
-
* @param {Color} [color=WHITE]
|
|
6216
|
-
* @param {number} [angle] */
|
|
6217
|
-
drawRect(pos, size, color, angle)
|
|
6218
|
-
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
6219
|
-
|
|
6220
6425
|
/** Create WebGL texture if necessary and copy layer canvas to it */
|
|
6221
6426
|
updateWebGL()
|
|
6222
6427
|
{ this.textureInfo.createWebGLTexture(); }
|
|
@@ -6272,6 +6477,8 @@ class TileLayer extends CanvasLayer
|
|
|
6272
6477
|
this.redrawTileData = ()=> {};
|
|
6273
6478
|
this.drawLayerTile = ()=> {};
|
|
6274
6479
|
this.drawLayerRect = ()=> {};
|
|
6480
|
+
this.drawTile = ()=> {};
|
|
6481
|
+
this.drawRect = ()=> {};
|
|
6275
6482
|
this.clearLayerRect = ()=> {};
|
|
6276
6483
|
return;
|
|
6277
6484
|
}
|
|
@@ -6299,7 +6506,7 @@ class TileLayer extends CanvasLayer
|
|
|
6299
6506
|
ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
|
|
6300
6507
|
|
|
6301
6508
|
if (!layerPos.arrayCheck(this.size)) return;
|
|
6302
|
-
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;
|
|
6303
6510
|
|
|
6304
6511
|
if (!redraw) return;
|
|
6305
6512
|
const isRedraw = drawContext === this.context;
|
|
@@ -6314,11 +6521,11 @@ class TileLayer extends CanvasLayer
|
|
|
6314
6521
|
|
|
6315
6522
|
/** Get data at a given position in the array
|
|
6316
6523
|
* @param {Vector2} layerPos - Local position in array
|
|
6317
|
-
* @return {TileLayerData} */
|
|
6524
|
+
* @return {TileLayerData|undefined} */
|
|
6318
6525
|
getData(layerPos)
|
|
6319
|
-
{
|
|
6526
|
+
{
|
|
6320
6527
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
6321
|
-
return layerPos.arrayCheck(this.size)
|
|
6528
|
+
return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
|
|
6322
6529
|
}
|
|
6323
6530
|
|
|
6324
6531
|
// Update the tile layer, refresh texture if needed
|
|
@@ -6426,7 +6633,7 @@ class TileLayer extends CanvasLayer
|
|
|
6426
6633
|
|
|
6427
6634
|
// draw the tile if it has layer data
|
|
6428
6635
|
const d = this.getData(layerPos);
|
|
6429
|
-
if (!d.tile) return;
|
|
6636
|
+
if (!d || !d.tile) return;
|
|
6430
6637
|
|
|
6431
6638
|
const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
|
|
6432
6639
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -6472,6 +6679,38 @@ class TileLayer extends CanvasLayer
|
|
|
6472
6679
|
drawLayerRect(pos, size, color, angle=0)
|
|
6473
6680
|
{ this.drawLayerTile(pos, size, undefined, color, angle); }
|
|
6474
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
|
+
|
|
6475
6714
|
/** Clear a rectangle in layer space
|
|
6476
6715
|
* @param {Vector2} pos - position in pixel coordinates
|
|
6477
6716
|
* @param {Vector2} size
|
|
@@ -6550,7 +6789,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6550
6789
|
setCollisionData(layerPos, data=1)
|
|
6551
6790
|
{
|
|
6552
6791
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
6553
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
6792
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
6554
6793
|
layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
|
|
6555
6794
|
}
|
|
6556
6795
|
|
|
@@ -6565,7 +6804,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6565
6804
|
getCollisionData(layerPos)
|
|
6566
6805
|
{
|
|
6567
6806
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
6568
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
6807
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
6569
6808
|
return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
|
|
6570
6809
|
}
|
|
6571
6810
|
|
|
@@ -6590,8 +6829,10 @@ class TileCollisionLayer extends TileLayer
|
|
|
6590
6829
|
const posY = pos.y - this.pos.y;
|
|
6591
6830
|
const minX = max(posX - size.x/2|0, 0);
|
|
6592
6831
|
const minY = max(posY - size.y/2|0, 0);
|
|
6593
|
-
|
|
6594
|
-
|
|
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);
|
|
6595
6836
|
const hitPos = new Vector2;
|
|
6596
6837
|
for (let y = minY; y < maxY; ++y)
|
|
6597
6838
|
for (let x = minX; x < maxX; ++x)
|
|
@@ -6710,7 +6951,7 @@ class ParticleEmitter extends EngineObject
|
|
|
6710
6951
|
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
6711
6952
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
6712
6953
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
6713
|
-
* @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
|
|
6714
6955
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
6715
6956
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
6716
6957
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
@@ -6794,7 +7035,7 @@ class ParticleEmitter extends EngineObject
|
|
|
6794
7035
|
this.gravityScale = gravityScale;
|
|
6795
7036
|
/** @property {number} - Cone for start particle angle */
|
|
6796
7037
|
this.particleConeAngle = particleConeAngle;
|
|
6797
|
-
/** @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) */
|
|
6798
7039
|
this.fadeRate = fadeRate;
|
|
6799
7040
|
/** @property {number} - Apply extra randomness percent */
|
|
6800
7041
|
this.randomness = randomness;
|
|
@@ -7072,33 +7313,32 @@ class Particle
|
|
|
7072
7313
|
const hitLayer = tileCollisionTest(this.pos);
|
|
7073
7314
|
if (!testCollision(oldPos))
|
|
7074
7315
|
{
|
|
7075
|
-
|
|
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)
|
|
7076
7324
|
{
|
|
7077
|
-
//
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
const hitFriction = max(friction, hitLayer.friction);
|
|
7082
|
-
if (isBlockedX)
|
|
7083
|
-
{
|
|
7084
|
-
// move to previous X position and bounce
|
|
7085
|
-
this.pos.x = oldPos.x;
|
|
7086
|
-
this.velocity.x *= -hitRestitution;
|
|
7087
|
-
this.velocity.y *= hitFriction;
|
|
7088
|
-
}
|
|
7089
|
-
if (isBlockedY || !isBlockedX)
|
|
7090
|
-
{
|
|
7091
|
-
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
7092
|
-
if (wasFalling)
|
|
7093
|
-
this.groundObject = hitLayer;
|
|
7094
|
-
|
|
7095
|
-
// move to previous Y position and bounce
|
|
7096
|
-
this.pos.y = oldPos.y;
|
|
7097
|
-
this.velocity.y *= -hitRestitution;
|
|
7098
|
-
this.velocity.x *= hitFriction;
|
|
7099
|
-
}
|
|
7100
|
-
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;
|
|
7101
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');
|
|
7102
7342
|
}
|
|
7103
7343
|
}
|
|
7104
7344
|
}
|
|
@@ -7259,6 +7499,10 @@ function glInit(rootElement)
|
|
|
7259
7499
|
for (const info of glTextureInfos)
|
|
7260
7500
|
info.glTexture = undefined;
|
|
7261
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;
|
|
7262
7506
|
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
7263
7507
|
});
|
|
7264
7508
|
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
@@ -7617,6 +7861,10 @@ function glSetTextureData(texture, image)
|
|
|
7617
7861
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
7618
7862
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
7619
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
|
+
|
|
7620
7868
|
// rebind active texture
|
|
7621
7869
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
7622
7870
|
}
|
|
@@ -7662,7 +7910,7 @@ function glFlush()
|
|
|
7662
7910
|
{
|
|
7663
7911
|
if (glEnable && glContext && glBatchCount)
|
|
7664
7912
|
{
|
|
7665
|
-
// set
|
|
7913
|
+
// set blend mode
|
|
7666
7914
|
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
7667
7915
|
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
7668
7916
|
glContext.enable(glContext.BLEND);
|
|
@@ -7676,7 +7924,8 @@ function glFlush()
|
|
|
7676
7924
|
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
7677
7925
|
else
|
|
7678
7926
|
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
7679
|
-
drawCount
|
|
7927
|
+
++drawCount;
|
|
7928
|
+
primitiveCount += glBatchCount;
|
|
7680
7929
|
glBatchCount = 0;
|
|
7681
7930
|
}
|
|
7682
7931
|
glBatchAdditive = glAdditive;
|
|
@@ -7737,6 +7986,22 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
7737
7986
|
glPositionData[offset++] = angle;
|
|
7738
7987
|
}
|
|
7739
7988
|
|
|
7989
|
+
/** Add an untextured rect to the gl draw list
|
|
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.
|
|
7993
|
+
* @param {number} x
|
|
7994
|
+
* @param {number} y
|
|
7995
|
+
* @param {number} sizeX
|
|
7996
|
+
* @param {number} sizeY
|
|
7997
|
+
* @param {number} angle
|
|
7998
|
+
* @param {number} rgba - color as 32-bit integer
|
|
7999
|
+
* @memberof WebGL */
|
|
8000
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
8001
|
+
{
|
|
8002
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
8003
|
+
}
|
|
8004
|
+
|
|
7740
8005
|
/** Transform and add a polygon to the gl draw list
|
|
7741
8006
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
7742
8007
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -7750,13 +8015,13 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
7750
8015
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
7751
8016
|
{
|
|
7752
8017
|
const pointsOut = [];
|
|
8018
|
+
const sa = sin(-angle);
|
|
8019
|
+
const ca = cos(-angle);
|
|
7753
8020
|
for (const p of points)
|
|
7754
8021
|
{
|
|
7755
8022
|
// transform the point
|
|
7756
8023
|
const px = p.x*sx;
|
|
7757
8024
|
const py = p.y*sy;
|
|
7758
|
-
const sa = sin(-angle);
|
|
7759
|
-
const ca = cos(-angle);
|
|
7760
8025
|
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
7761
8026
|
}
|
|
7762
8027
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
@@ -7788,11 +8053,13 @@ function glDrawPoints(points, rgba)
|
|
|
7788
8053
|
{
|
|
7789
8054
|
if (!glEnable || points.length < 3)
|
|
7790
8055
|
return; // needs at least 3 points to have area
|
|
7791
|
-
|
|
8056
|
+
|
|
7792
8057
|
// flush if there is not enough room or if different blend mode
|
|
7793
8058
|
const vertCount = points.length + 2;
|
|
7794
8059
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
7795
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
|
|
7796
8063
|
glSetPolyMode();
|
|
7797
8064
|
|
|
7798
8065
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -7816,11 +8083,13 @@ function glDrawColoredPoints(points, pointColors)
|
|
|
7816
8083
|
{
|
|
7817
8084
|
if (!glEnable || points.length < 3)
|
|
7818
8085
|
return; // needs at least 3 points to have area
|
|
7819
|
-
|
|
8086
|
+
|
|
7820
8087
|
// flush if there is not enough room or if different blend mode
|
|
7821
8088
|
const vertCount = points.length + 2;
|
|
7822
8089
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
7823
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
|
|
7824
8093
|
glSetPolyMode();
|
|
7825
8094
|
|
|
7826
8095
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -7890,7 +8159,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
7890
8159
|
const strip = [];
|
|
7891
8160
|
const n = points.length;
|
|
7892
8161
|
const e = 1e-6;
|
|
7893
|
-
|
|
8162
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
8163
|
+
const miterLimit = 10;
|
|
7894
8164
|
for (let i = 0; i < n; i++)
|
|
7895
8165
|
{
|
|
7896
8166
|
// for each vertex, calculate normal based on adjacent edges
|
|
@@ -8146,7 +8416,7 @@ function drawEngineLogo(t)
|
|
|
8146
8416
|
x.closePath();
|
|
8147
8417
|
gradient(0, Y, 0, Y+H,C);
|
|
8148
8418
|
}
|
|
8149
|
-
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';
|
|
8150
8420
|
|
|
8151
8421
|
// center and fit to screen
|
|
8152
8422
|
const alpha = oscillate(1,1,t);
|
|
@@ -8373,7 +8643,8 @@ class Medal
|
|
|
8373
8643
|
/** @property {boolean} - Is the medal unlocked? */
|
|
8374
8644
|
this.unlocked = false;
|
|
8375
8645
|
|
|
8376
|
-
|
|
8646
|
+
/** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
|
|
8647
|
+
this.image = undefined;
|
|
8377
8648
|
if (src)
|
|
8378
8649
|
(this.image = new Image).src = src;
|
|
8379
8650
|
|
|
@@ -8536,13 +8807,18 @@ class NewgroundsPlugin
|
|
|
8536
8807
|
ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
|
|
8537
8808
|
|
|
8538
8809
|
newgrounds = this; // set global newgrounds object
|
|
8810
|
+
/** @property {string} - The newgrounds App ID */
|
|
8539
8811
|
this.app_id = app_id;
|
|
8812
|
+
/** @property {string|undefined} - AES-128/Base64 encryption key, if any */
|
|
8540
8813
|
this.cipher = cipher;
|
|
8814
|
+
/** @property {Object|undefined} - CryptoJS instance used when cipher is set */
|
|
8541
8815
|
this.cryptoJS = cryptoJS;
|
|
8816
|
+
/** @property {string} - Hostname used when logging views */
|
|
8542
8817
|
this.host = location ? location.hostname : '';
|
|
8543
8818
|
|
|
8544
8819
|
// get session id from url search params
|
|
8545
8820
|
const url = new URL(location.href);
|
|
8821
|
+
/** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
|
|
8546
8822
|
this.session_id = url.searchParams.get('ngio_session_id');
|
|
8547
8823
|
|
|
8548
8824
|
if (!this.session_id)
|
|
@@ -8550,7 +8826,20 @@ class NewgroundsPlugin
|
|
|
8550
8826
|
|
|
8551
8827
|
// get medals
|
|
8552
8828
|
const medalsResult = this.call('Medal.getList');
|
|
8553
|
-
|
|
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
|
+
|
|
8841
|
+
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
8842
|
+
this.medals = medalsResult.result.data?.['medals'] || [];
|
|
8554
8843
|
debugMedals && LOG(this.medals);
|
|
8555
8844
|
for (const newgroundsMedal of this.medals)
|
|
8556
8845
|
{
|
|
@@ -8570,10 +8859,11 @@ class NewgroundsPlugin
|
|
|
8570
8859
|
medal.description = medal.description + ` (${ medal.value })`;
|
|
8571
8860
|
}
|
|
8572
8861
|
}
|
|
8573
|
-
|
|
8862
|
+
|
|
8574
8863
|
// get scoreboards
|
|
8575
8864
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
8576
|
-
|
|
8865
|
+
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
8866
|
+
this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
|
|
8577
8867
|
debugMedals && LOG(this.scoreboards);
|
|
8578
8868
|
|
|
8579
8869
|
// keep the session alive with a ping every minute
|
|
@@ -8757,10 +9047,14 @@ class PostProcessPlugin
|
|
|
8757
9047
|
function postProcessRender()
|
|
8758
9048
|
{
|
|
8759
9049
|
if (headlessMode || !glEnable) return;
|
|
8760
|
-
|
|
9050
|
+
|
|
8761
9051
|
// clear out the buffer
|
|
8762
9052
|
glFlush();
|
|
8763
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
|
+
|
|
8764
9058
|
// setup shader program to draw a quad
|
|
8765
9059
|
glContext.useProgram(postProcess.shader);
|
|
8766
9060
|
glContext.bindVertexArray(postProcess.vao);
|
|
@@ -8801,6 +9095,9 @@ class PostProcessPlugin
|
|
|
8801
9095
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
|
|
8802
9096
|
}
|
|
8803
9097
|
|
|
9098
|
+
// restore default so subsequent dynamic texture uploads aren't flipped
|
|
9099
|
+
glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
|
|
9100
|
+
|
|
8804
9101
|
// force it to set instanced mode
|
|
8805
9102
|
glSetInstancedMode(true);
|
|
8806
9103
|
}
|
|
@@ -9427,11 +9724,17 @@ class UISystemPlugin
|
|
|
9427
9724
|
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
9428
9725
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
9429
9726
|
{
|
|
9430
|
-
|
|
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)=>
|
|
9431
9733
|
{
|
|
9432
|
-
|
|
9734
|
+
const listener = (e)=> { e.preventDefault(); callback && callback(e); };
|
|
9433
9735
|
document.addEventListener(listenerType, listener);
|
|
9434
|
-
|
|
9736
|
+
this._dragListeners.push([listenerType, listener]);
|
|
9737
|
+
};
|
|
9435
9738
|
setCallback(onDrop, 'drop');
|
|
9436
9739
|
setCallback(onDragEnter, 'dragenter');
|
|
9437
9740
|
setCallback(onDragLeave, 'dragleave');
|
|
@@ -9755,6 +10058,15 @@ class UIObject
|
|
|
9755
10058
|
if (this.destroyed)
|
|
9756
10059
|
return;
|
|
9757
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
|
+
|
|
9758
10070
|
// disconnect from parent and destroy children
|
|
9759
10071
|
this.destroyed = 1;
|
|
9760
10072
|
this.parent?.removeChild(this);
|
|
@@ -9763,6 +10075,8 @@ class UIObject
|
|
|
9763
10075
|
child.parent = undefined;
|
|
9764
10076
|
child.destroy();
|
|
9765
10077
|
}
|
|
10078
|
+
// clear references so destroyed children can be GC'd
|
|
10079
|
+
this.children.length = 0;
|
|
9766
10080
|
}
|
|
9767
10081
|
|
|
9768
10082
|
/** Check if the mouse is overlapping this ui object
|
|
@@ -10352,6 +10666,7 @@ class UISlider extends UIObject
|
|
|
10352
10666
|
{
|
|
10353
10667
|
// toggle value between 0 and 1
|
|
10354
10668
|
this.value = this.value ? 0 : 1;
|
|
10669
|
+
this.onChange();
|
|
10355
10670
|
this.onRelease();
|
|
10356
10671
|
super.navigatePressed();
|
|
10357
10672
|
}
|
|
@@ -10711,6 +11026,11 @@ class Box2dObject extends EngineObject
|
|
|
10711
11026
|
// destroy physics body, fixtures, and joints
|
|
10712
11027
|
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
10713
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);
|
|
10714
11034
|
super.destroy();
|
|
10715
11035
|
}
|
|
10716
11036
|
|
|
@@ -10799,7 +11119,9 @@ class Box2dObject extends EngineObject
|
|
|
10799
11119
|
/** Add a box shape to the body
|
|
10800
11120
|
* @param {Vector2} [size]
|
|
10801
11121
|
* @param {Vector2} [offset]
|
|
10802
|
-
* @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.
|
|
10803
11125
|
* @param {number} [density]
|
|
10804
11126
|
* @param {number} [friction]
|
|
10805
11127
|
* @param {number} [restitution]
|
|
@@ -10812,7 +11134,7 @@ class Box2dObject extends EngineObject
|
|
|
10812
11134
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
10813
11135
|
|
|
10814
11136
|
const shape = new box2d.instance.b2PolygonShape();
|
|
10815
|
-
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);
|
|
10816
11138
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
10817
11139
|
}
|
|
10818
11140
|
|
|
@@ -11114,9 +11436,10 @@ class Box2dObject extends EngineObject
|
|
|
11114
11436
|
{
|
|
11115
11437
|
const data = new box2d.instance.b2MassData();
|
|
11116
11438
|
this.body.GetMassData(data);
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
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);
|
|
11120
11443
|
this.body.SetMassData(data);
|
|
11121
11444
|
}
|
|
11122
11445
|
|
|
@@ -12288,6 +12611,8 @@ class Box2dPlugin
|
|
|
12288
12611
|
const fixtureB = contact.GetFixtureB();
|
|
12289
12612
|
const objectA = fixtureA.GetBody().object;
|
|
12290
12613
|
const objectB = fixtureB.GetBody().object;
|
|
12614
|
+
// raw user-created b2Bodies may have no .object — skip those
|
|
12615
|
+
if (!objectA || !objectB) return;
|
|
12291
12616
|
objectA.beginContact(objectB);
|
|
12292
12617
|
objectB.beginContact(objectA);
|
|
12293
12618
|
}
|
|
@@ -12298,6 +12623,7 @@ class Box2dPlugin
|
|
|
12298
12623
|
const fixtureB = contact.GetFixtureB();
|
|
12299
12624
|
const objectA = fixtureA.GetBody().object;
|
|
12300
12625
|
const objectB = fixtureB.GetBody().object;
|
|
12626
|
+
if (!objectA || !objectB) return;
|
|
12301
12627
|
objectA.endContact(objectB);
|
|
12302
12628
|
objectB.endContact(objectA);
|
|
12303
12629
|
};
|
|
@@ -12676,7 +13002,7 @@ async function box2dInit()
|
|
|
12676
13002
|
debugDraw.DrawTransform = function(transform)
|
|
12677
13003
|
{
|
|
12678
13004
|
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
|
|
12679
|
-
const pos =
|
|
13005
|
+
const pos = box2d.vec2From(transform.get_p());
|
|
12680
13006
|
const angle = -transform.get_q().GetAngle();
|
|
12681
13007
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
12682
13008
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
|
@@ -12885,13 +13211,21 @@ class Tween
|
|
|
12885
13211
|
}
|
|
12886
13212
|
ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
|
|
12887
13213
|
|
|
13214
|
+
/** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
|
|
12888
13215
|
this.callback = callback;
|
|
13216
|
+
/** @property {number|Vector2|Color} - Starting value */
|
|
12889
13217
|
this.start = start;
|
|
13218
|
+
/** @property {number|Vector2|Color} - Ending value */
|
|
12890
13219
|
this.end = end;
|
|
13220
|
+
/** @property {number} - Total duration in seconds */
|
|
12891
13221
|
this.duration = duration;
|
|
13222
|
+
/** @property {number} - Remaining time in seconds (counts down from duration to 0) */
|
|
12892
13223
|
this.life = duration;
|
|
13224
|
+
/** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
|
|
12893
13225
|
this.ease = options.ease || Ease.LINEAR;
|
|
13226
|
+
/** @property {boolean} - If true, advance even when the game is paused */
|
|
12894
13227
|
this.useRealTime = !!options.useRealTime;
|
|
13228
|
+
/** @property {boolean} - If true, stop advancing until cleared */
|
|
12895
13229
|
this.paused = !!options.paused;
|
|
12896
13230
|
|
|
12897
13231
|
/** @private completion callback set by then(), loop(), pingPong(). */
|
|
@@ -13072,7 +13406,7 @@ const Ease =
|
|
|
13072
13406
|
* @param {number} x
|
|
13073
13407
|
* @returns {number}
|
|
13074
13408
|
* @memberof TweenSystem */
|
|
13075
|
-
EXPO: (x) => 2 ** (10 * x - 10),
|
|
13409
|
+
EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
|
|
13076
13410
|
|
|
13077
13411
|
/** Back ease-in: overshoots backward at the start before snapping forward.
|
|
13078
13412
|
* @param {number} x
|
|
@@ -13085,6 +13419,8 @@ const Ease =
|
|
|
13085
13419
|
* @returns {number}
|
|
13086
13420
|
* @memberof TweenSystem */
|
|
13087
13421
|
ELASTIC: (x) =>
|
|
13422
|
+
x === 0 ? 0 :
|
|
13423
|
+
x === 1 ? 1 :
|
|
13088
13424
|
-(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
|
|
13089
13425
|
|
|
13090
13426
|
/** Spring-like ease-out: oscillates outward after passing the target.
|
|
@@ -13245,29 +13581,32 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
13245
13581
|
}
|
|
13246
13582
|
|
|
13247
13583
|
// Continuation that schedules the next loop iteration when one finishes.
|
|
13248
|
-
//
|
|
13249
|
-
//
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
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));
|
|
13271
13610
|
}
|
|
13272
13611
|
|
|
13273
13612
|
/** Engine plugin hook: advance every active tween by the appropriate delta.
|
|
@@ -13424,7 +13763,9 @@ class PathFinder
|
|
|
13424
13763
|
// .size + .getCollisionData.
|
|
13425
13764
|
if (isVector2(source))
|
|
13426
13765
|
{
|
|
13766
|
+
/** @property {Vector2} - Grid dimensions in tiles */
|
|
13427
13767
|
this.size = source.floor();
|
|
13768
|
+
/** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
|
|
13428
13769
|
this.tileLayer = undefined;
|
|
13429
13770
|
}
|
|
13430
13771
|
else
|
|
@@ -13436,13 +13777,18 @@ class PathFinder
|
|
|
13436
13777
|
}
|
|
13437
13778
|
|
|
13438
13779
|
// Tunables (public, freely re-assignable).
|
|
13780
|
+
/** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
|
|
13439
13781
|
this.heuristicWeight = 1;
|
|
13440
|
-
|
|
13782
|
+
/** @property {number} - Maximum A* expansions before giving up */
|
|
13783
|
+
this.maxLoop = 1e3;
|
|
13784
|
+
/** @property {boolean} - If true, post-process paths with two-pass smoothing */
|
|
13441
13785
|
this.smoothPath = true;
|
|
13786
|
+
/** @property {boolean} - If true, draw debug visualization during findPath */
|
|
13442
13787
|
this.debug = false;
|
|
13443
|
-
|
|
13788
|
+
/** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
|
|
13789
|
+
this.debugTime = 1;
|
|
13444
13790
|
|
|
13445
|
-
|
|
13791
|
+
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
|
|
13446
13792
|
this.nodes = new Array(this.size.x * this.size.y);
|
|
13447
13793
|
for (let y = 0; y < this.size.y; ++y)
|
|
13448
13794
|
for (let x = 0; x < this.size.x; ++x)
|
|
@@ -13594,11 +13940,13 @@ class PathFinder
|
|
|
13594
13940
|
if (dx !== 0 && dy !== 0)
|
|
13595
13941
|
{
|
|
13596
13942
|
// Diagonal step: refuse if either cardinal neighbor is
|
|
13597
|
-
// 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*.)
|
|
13598
13946
|
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
13599
|
-
if (!card1 ||
|
|
13947
|
+
if (!card1 || !card1.walkable) continue;
|
|
13600
13948
|
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
13601
|
-
if (!card2 ||
|
|
13949
|
+
if (!card2 || !card2.walkable) continue;
|
|
13602
13950
|
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
13603
13951
|
}
|
|
13604
13952
|
|
|
@@ -13616,9 +13964,12 @@ class PathFinder
|
|
|
13616
13964
|
// Best path so far through neighbor — record it.
|
|
13617
13965
|
neighbor.parent = current;
|
|
13618
13966
|
neighbor.g = tentativeG;
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
13967
|
+
// Octile heuristic — tightest admissible distance for an
|
|
13968
|
+
// 8-connected grid with cardinal cost 1 and diagonal cost √2.
|
|
13969
|
+
const adx = abs(endNode.pos.x - neighbor.pos.x);
|
|
13970
|
+
const ady = abs(endNode.pos.y - neighbor.pos.y);
|
|
13971
|
+
const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
|
|
13972
|
+
neighbor.f = neighbor.g + h * this.heuristicWeight;
|
|
13622
13973
|
}
|
|
13623
13974
|
}
|
|
13624
13975
|
|
|
@@ -13900,6 +14251,24 @@ class PathFinder
|
|
|
13900
14251
|
path.push(original[original.length - 1]);
|
|
13901
14252
|
}
|
|
13902
14253
|
|
|
14254
|
+
/** Drop any middle node that lies exactly on the line through its two
|
|
14255
|
+
* neighbors. Backstop for the smoothing passes — the corners pass
|
|
14256
|
+
* intentionally keeps truly-straight runs, and the string-pulling pass
|
|
14257
|
+
* checks collinearity against the original path, not the in-progress
|
|
14258
|
+
* result, so it can leave 3+ collinear nodes in some edge cases.
|
|
14259
|
+
* @param {PathFinderNode[]} path
|
|
14260
|
+
* @private */
|
|
14261
|
+
dropCollinearNodes(path)
|
|
14262
|
+
{
|
|
14263
|
+
for (let i = path.length - 2; i >= 1; --i)
|
|
14264
|
+
{
|
|
14265
|
+
const a = path[i - 1], b = path[i], c = path[i + 1];
|
|
14266
|
+
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
|
|
14267
|
+
(b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
|
|
14268
|
+
path.splice(i, 1);
|
|
14269
|
+
}
|
|
14270
|
+
}
|
|
14271
|
+
|
|
13903
14272
|
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
13904
14273
|
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
13905
14274
|
* @param {number} x
|
|
@@ -14067,6 +14436,7 @@ class PathFinder
|
|
|
14067
14436
|
{
|
|
14068
14437
|
this.smoothPathCorners(nodePath);
|
|
14069
14438
|
this.smoothPathStringPull(nodePath);
|
|
14439
|
+
this.dropCollinearNodes(nodePath);
|
|
14070
14440
|
}
|
|
14071
14441
|
|
|
14072
14442
|
// Convert to world-space Vector2 path. Return copies, not live node
|