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.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
|
{
|
|
@@ -649,12 +661,8 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
|
|
|
649
661
|
ASSERT(isNumber(time), 'time must be a number');
|
|
650
662
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
651
663
|
|
|
652
|
-
if (typeof size === 'number')
|
|
653
|
-
size = vec2(size); // allow passing in floats
|
|
654
664
|
if (isColor(color))
|
|
655
665
|
color = color.toString();
|
|
656
|
-
pos = pos.copy();
|
|
657
|
-
size = size.copy();
|
|
658
666
|
const timer = new Timer(time);
|
|
659
667
|
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
|
|
660
668
|
}
|
|
@@ -724,7 +732,7 @@ function debugPoint(pos, color, time, angle, screenSpace=false)
|
|
|
724
732
|
* @param {number} [time]
|
|
725
733
|
* @param {boolean} [screenSpace]
|
|
726
734
|
* @memberof Debug */
|
|
727
|
-
function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
|
|
735
|
+
function debugLine(posA, posB, color, width=.1, time=0, screenSpace=false)
|
|
728
736
|
{
|
|
729
737
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
730
738
|
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
@@ -762,7 +770,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
762
770
|
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
|
|
763
771
|
}
|
|
764
772
|
|
|
765
|
-
/** Draw
|
|
773
|
+
/** Draw debug text in world space
|
|
766
774
|
* @param {string|number} text
|
|
767
775
|
* @param {Vector2} pos
|
|
768
776
|
* @param {number} [size]
|
|
@@ -1068,7 +1076,8 @@ function debugRender()
|
|
|
1068
1076
|
debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
|
|
1069
1077
|
x, y += h);
|
|
1070
1078
|
debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
|
|
1071
|
-
debugContext.fillText('Draw
|
|
1079
|
+
debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
|
|
1080
|
+
debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
|
|
1072
1081
|
debugContext.fillText('---------', x, y += h);
|
|
1073
1082
|
debugContext.fillStyle = '#f00';
|
|
1074
1083
|
debugContext.fillText('ESC: Debug Overlay', x, y += h);
|
|
@@ -1092,7 +1101,7 @@ function debugRender()
|
|
|
1092
1101
|
continue;
|
|
1093
1102
|
if (parseInt(i) < 3)
|
|
1094
1103
|
mousePressed += i + ' ' ;
|
|
1095
|
-
else
|
|
1104
|
+
else
|
|
1096
1105
|
keysPressed += i + ' ' ;
|
|
1097
1106
|
}
|
|
1098
1107
|
mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
|
|
@@ -1139,7 +1148,8 @@ function debugRenderPost()
|
|
|
1139
1148
|
mainContext.font = '1em monospace';
|
|
1140
1149
|
mainContext.fillStyle = '#000';
|
|
1141
1150
|
const text = engineName + ' v' + engineVersion + ' / '
|
|
1142
|
-
+ drawCount + ' / ' +
|
|
1151
|
+
+ drawCount + ' / ' + primitiveCount + ' / '
|
|
1152
|
+
+ engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
1143
1153
|
+ (glEnable ? ' GL' : ' 2D') ;
|
|
1144
1154
|
mainContext.fillText(text, mainCanvas.width-3, 3);
|
|
1145
1155
|
mainContext.fillStyle = '#fff';
|
|
@@ -1350,19 +1360,19 @@ const max = Math.max;
|
|
|
1350
1360
|
* @param {number} x
|
|
1351
1361
|
* @return {number}
|
|
1352
1362
|
* @memberof Math */
|
|
1353
|
-
const sign = Math.sign;
|
|
1363
|
+
const sign = (x) => Math.sign(x);
|
|
1354
1364
|
|
|
1355
1365
|
/** Returns hypotenuse of values passed in
|
|
1356
1366
|
* @param {...number} values
|
|
1357
1367
|
* @return {number}
|
|
1358
1368
|
* @memberof Math */
|
|
1359
|
-
const hypot = Math.hypot;
|
|
1369
|
+
const hypot = (...values) => Math.hypot(...values);
|
|
1360
1370
|
|
|
1361
1371
|
/** Returns log2 of value passed in
|
|
1362
1372
|
* @param {number} x
|
|
1363
1373
|
* @return {number}
|
|
1364
1374
|
* @memberof Math */
|
|
1365
|
-
const log2 = Math.log2;
|
|
1375
|
+
const log2 = (x) => Math.log2(x);
|
|
1366
1376
|
|
|
1367
1377
|
/** Returns sin of value passed in
|
|
1368
1378
|
* @param {number} x
|
|
@@ -1504,7 +1514,8 @@ function isOverlapping(posA, sizeA, posB, sizeB=vec2())
|
|
|
1504
1514
|
const dy = (posA.y - posB.y)*2;
|
|
1505
1515
|
const sx = sizeA.x + sizeB.x;
|
|
1506
1516
|
const sy = sizeA.y + sizeB.y;
|
|
1507
|
-
|
|
1517
|
+
// symmetric so isOverlapping(A,B) === isOverlapping(B,A) at touching edges
|
|
1518
|
+
return abs(dx) < sx && abs(dy) < sy;
|
|
1508
1519
|
}
|
|
1509
1520
|
|
|
1510
1521
|
/** Returns true if a line segment is intersecting an axis aligned box
|
|
@@ -1593,7 +1604,7 @@ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'
|
|
|
1593
1604
|
/**
|
|
1594
1605
|
* Check if object is an array
|
|
1595
1606
|
* @param {any} a
|
|
1596
|
-
* @return {
|
|
1607
|
+
* @return {a is Array<any>}
|
|
1597
1608
|
* @memberof Math */
|
|
1598
1609
|
function isArray(a) { return Array.isArray(a); }
|
|
1599
1610
|
|
|
@@ -1731,7 +1742,13 @@ function randVec2(length=1) { return new Vector2().setAngle(rand(2*PI), length);
|
|
|
1731
1742
|
* @return {Vector2}
|
|
1732
1743
|
* @memberof Random */
|
|
1733
1744
|
function randInCircle(radius=1, minRadius=0)
|
|
1734
|
-
{
|
|
1745
|
+
{
|
|
1746
|
+
// r is uniform in area ⇒ r² uniform in [minRadius², radius²]
|
|
1747
|
+
// (the squared inner bound is what makes minRadius the actual exclusion edge)
|
|
1748
|
+
if (radius <= 0) return new Vector2;
|
|
1749
|
+
const ratio = clamp(minRadius / radius);
|
|
1750
|
+
return randVec2(radius * rand(ratio*ratio, 1)**.5);
|
|
1751
|
+
}
|
|
1735
1752
|
|
|
1736
1753
|
/** Returns a random color between the two passed in colors, combine components if linear
|
|
1737
1754
|
* @param {Color} [colorA=WHITE]
|
|
@@ -1801,7 +1818,14 @@ class RandomGenerator
|
|
|
1801
1818
|
* @param {number} [valueA]
|
|
1802
1819
|
* @param {number} [valueB]
|
|
1803
1820
|
* @return {number} */
|
|
1804
|
-
floatSign(valueA=1, valueB=0)
|
|
1821
|
+
floatSign(valueA=1, valueB=0)
|
|
1822
|
+
{
|
|
1823
|
+
const lo = min(valueA, valueB);
|
|
1824
|
+
const hi = max(valueA, valueB);
|
|
1825
|
+
const d = hi - lo;
|
|
1826
|
+
const e = this.float(d*2);
|
|
1827
|
+
return e < d ? lo + e : d - lo - e;
|
|
1828
|
+
}
|
|
1805
1829
|
|
|
1806
1830
|
/** Returns a random angle between -PI and PI
|
|
1807
1831
|
* @return {number} */
|
|
@@ -2449,6 +2473,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
2449
2473
|
* - File saving (text, canvas, data URLs)
|
|
2450
2474
|
* - Native share dialog support
|
|
2451
2475
|
* - Local storage save data management
|
|
2476
|
+
* - Gradient noise (1D and 2D)
|
|
2452
2477
|
* @namespace Utilities
|
|
2453
2478
|
*/
|
|
2454
2479
|
|
|
@@ -2513,9 +2538,15 @@ class Timer
|
|
|
2513
2538
|
* @return {number} */
|
|
2514
2539
|
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2515
2540
|
|
|
2516
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2541
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
2542
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
2517
2543
|
* @return {number} */
|
|
2518
|
-
getPercent()
|
|
2544
|
+
getPercent()
|
|
2545
|
+
{
|
|
2546
|
+
if (!this.isSet()) return 0;
|
|
2547
|
+
if (!this.setTime) return 1;
|
|
2548
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
2549
|
+
}
|
|
2519
2550
|
|
|
2520
2551
|
/** Get the time this timer was set to, returns 0 if not set
|
|
2521
2552
|
* @return {number} */
|
|
@@ -2542,9 +2573,9 @@ class Timer
|
|
|
2542
2573
|
* @memberof Utilities */
|
|
2543
2574
|
function formatTime(t)
|
|
2544
2575
|
{
|
|
2545
|
-
const
|
|
2576
|
+
const signStr = t < 0 ? '-' : '';
|
|
2546
2577
|
t = abs(t)|0;
|
|
2547
|
-
return
|
|
2578
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
2548
2579
|
}
|
|
2549
2580
|
|
|
2550
2581
|
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
@@ -2630,15 +2661,20 @@ function shareURL(title, url, callback)
|
|
|
2630
2661
|
function readSaveData(saveName, defaultSaveData)
|
|
2631
2662
|
{
|
|
2632
2663
|
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
2633
|
-
|
|
2634
|
-
//
|
|
2635
|
-
|
|
2664
|
+
|
|
2665
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
2666
|
+
// iframes) and corrupt JSON in stored data
|
|
2636
2667
|
let loadedData = {};
|
|
2637
|
-
|
|
2668
|
+
try
|
|
2638
2669
|
{
|
|
2639
|
-
|
|
2640
|
-
|
|
2670
|
+
const data = localStorage[saveName];
|
|
2671
|
+
if (data)
|
|
2672
|
+
{
|
|
2673
|
+
try { loadedData = JSON.parse(data); }
|
|
2674
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
2675
|
+
}
|
|
2641
2676
|
}
|
|
2677
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
2642
2678
|
return { ...defaultSaveData, ...loadedData };
|
|
2643
2679
|
}
|
|
2644
2680
|
|
|
@@ -2649,7 +2685,51 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
2649
2685
|
function writeSaveData(saveName, saveData)
|
|
2650
2686
|
{
|
|
2651
2687
|
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
2652
|
-
localStorage
|
|
2688
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
2689
|
+
try { localStorage[saveName] = JSON.stringify(saveData); }
|
|
2690
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2694
|
+
|
|
2695
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
2696
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
2697
|
+
function noiseHash(i)
|
|
2698
|
+
{
|
|
2699
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
2700
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
2701
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
2702
|
+
h ^= h >>> 16;
|
|
2703
|
+
return (h >>> 0) / 2**32;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
|
|
2707
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
2708
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
2709
|
+
* @param {number} x
|
|
2710
|
+
* @return {number}
|
|
2711
|
+
* @memberof Utilities */
|
|
2712
|
+
function noise1D(x)
|
|
2713
|
+
{
|
|
2714
|
+
const i = floor(x);
|
|
2715
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
/** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
2719
|
+
* @param {number} x
|
|
2720
|
+
* @param {number} y
|
|
2721
|
+
* @return {number}
|
|
2722
|
+
* @memberof Utilities */
|
|
2723
|
+
function noise2D(x, y)
|
|
2724
|
+
{
|
|
2725
|
+
const ix = floor(x), iy = floor(y);
|
|
2726
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
2727
|
+
// large prime decorrelates neighboring rows
|
|
2728
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
2729
|
+
return lerp(
|
|
2730
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
2731
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
2732
|
+
fy);
|
|
2653
2733
|
}
|
|
2654
2734
|
/**
|
|
2655
2735
|
* LittleJS Engine Settings
|
|
@@ -2702,7 +2782,7 @@ let canvasColorTiles = true;
|
|
|
2702
2782
|
|
|
2703
2783
|
/** Color to clear the canvas to before render, does not clear if alpha is 0
|
|
2704
2784
|
* @type {Color}
|
|
2705
|
-
* @memberof
|
|
2785
|
+
* @memberof Settings */
|
|
2706
2786
|
let canvasClearColor = CLEAR_BLACK;
|
|
2707
2787
|
|
|
2708
2788
|
/** The max size of the canvas, centered if window is larger
|
|
@@ -2902,7 +2982,8 @@ let touchInputEnable = true;
|
|
|
2902
2982
|
let touchGamepadEnable = false;
|
|
2903
2983
|
|
|
2904
2984
|
/** True if touch gamepad should have start button in the center
|
|
2905
|
-
* - Prevents activating
|
|
2985
|
+
* - Prevents activating within 2*touchGamepadSize of the virtual stick or face buttons
|
|
2986
|
+
* (one radius for the visible control + one radius of buffer beyond its edge)
|
|
2906
2987
|
* - When the game is paused, any touch will press the button
|
|
2907
2988
|
* - Set size to enable the center button
|
|
2908
2989
|
* @type {number}
|
|
@@ -3314,7 +3395,7 @@ class EngineObject
|
|
|
3314
3395
|
this.color = color.copy();
|
|
3315
3396
|
/** @property {Color} - Additive color to apply when rendered */
|
|
3316
3397
|
this.additiveColor = undefined;
|
|
3317
|
-
/** @property {boolean} - Should
|
|
3398
|
+
/** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
|
|
3318
3399
|
this.mirror = false;
|
|
3319
3400
|
/** @property {boolean} - Has object been destroyed? */
|
|
3320
3401
|
this.destroyed = false;
|
|
@@ -3400,6 +3481,9 @@ class EngineObject
|
|
|
3400
3481
|
// child objects do not have physics
|
|
3401
3482
|
ASSERT(!this.parent);
|
|
3402
3483
|
|
|
3484
|
+
// bail if a collision callback destroyed us mid-frame
|
|
3485
|
+
if (this.destroyed) return;
|
|
3486
|
+
|
|
3403
3487
|
if (this.clampSpeed)
|
|
3404
3488
|
{
|
|
3405
3489
|
// limit max speed to prevent missing collisions
|
|
@@ -3455,6 +3539,8 @@ class EngineObject
|
|
|
3455
3539
|
|
|
3456
3540
|
// notify objects of collision and check if should be resolved
|
|
3457
3541
|
const collide1 = this.collideWithObject(o);
|
|
3542
|
+
// callback may have destroyed us; stop resolving against more objects
|
|
3543
|
+
if (this.destroyed) return;
|
|
3458
3544
|
const collide2 = o.collideWithObject(this);
|
|
3459
3545
|
if (!collide1 || !collide2) continue;
|
|
3460
3546
|
|
|
@@ -3550,13 +3636,17 @@ class EngineObject
|
|
|
3550
3636
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
3551
3637
|
if (isBlockedX)
|
|
3552
3638
|
{
|
|
3553
|
-
// try to
|
|
3639
|
+
// try to step over a 1-tile bump (direction follows gravity sign
|
|
3640
|
+
// so inverted gravity steps down off a ceiling bump instead of up;
|
|
3641
|
+
// zero gravity defaults to the normal-gravity step-up direction)
|
|
3554
3642
|
const epsilon = 1e-3;
|
|
3555
|
-
const
|
|
3556
|
-
const
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3643
|
+
const maxMove = .1;
|
|
3644
|
+
const gravitySign = gravity.y > 0 ? -1 : 1;
|
|
3645
|
+
const y = gravitySign > 0 ?
|
|
3646
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
|
|
3647
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
|
|
3648
|
+
const delta = abs(y - this.pos.y);
|
|
3649
|
+
if (delta < maxMove)
|
|
3560
3650
|
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
3561
3651
|
{
|
|
3562
3652
|
this.pos.y = y;
|
|
@@ -3696,6 +3786,8 @@ class EngineObject
|
|
|
3696
3786
|
* @return {EngineObject} The child object added */
|
|
3697
3787
|
addChild(child, localPos=vec2(), localAngle=0)
|
|
3698
3788
|
{
|
|
3789
|
+
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
3790
|
+
if (this.destroyed) return child;
|
|
3699
3791
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
3700
3792
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
3701
3793
|
ASSERT(child !== this, 'cannot add self as child');
|
|
@@ -3712,10 +3804,7 @@ class EngineObject
|
|
|
3712
3804
|
removeChild(child)
|
|
3713
3805
|
{
|
|
3714
3806
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
3715
|
-
|
|
3716
|
-
const index = this.children.indexOf(child);
|
|
3717
|
-
ASSERT(index >= 0, 'child not found in children array');
|
|
3718
|
-
index >= 0 && this.children.splice(index, 1);
|
|
3807
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
3719
3808
|
child.parent = undefined;
|
|
3720
3809
|
}
|
|
3721
3810
|
|
|
@@ -3790,7 +3879,7 @@ class EngineObject
|
|
|
3790
3879
|
* - Optimized tile sheet sprite rendering using WebGL batching
|
|
3791
3880
|
* - Primitive drawing for polygons, ellipses, and lines
|
|
3792
3881
|
* - Tile-based rendering with TileInfo and TextureInfo classes
|
|
3793
|
-
* - Text rendering with custom fonts and
|
|
3882
|
+
* - Text rendering with custom fonts and ImageFont support
|
|
3794
3883
|
* - Color and additive color blending for effects
|
|
3795
3884
|
* - Rotation, mirroring, and scaling transformations
|
|
3796
3885
|
* - Camera system with position, scale, and rotation
|
|
@@ -3856,6 +3945,12 @@ let textureInfos = [];
|
|
|
3856
3945
|
* @memberof Draw */
|
|
3857
3946
|
let drawCount;
|
|
3858
3947
|
|
|
3948
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
3949
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
3950
|
+
* @type {number}
|
|
3951
|
+
* @memberof Draw */
|
|
3952
|
+
let primitiveCount;
|
|
3953
|
+
|
|
3859
3954
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
3860
3955
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
3861
3956
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
@@ -3880,7 +3975,7 @@ let drawCount;
|
|
|
3880
3975
|
* tile(1, 16, 3) // a tile at index 1 of size 16 on texture 3
|
|
3881
3976
|
* tile(vec2(4,8), vec2(30,10)) // a tile at index (4,8) with a size of (30,10)
|
|
3882
3977
|
* @memberof Draw */
|
|
3883
|
-
function tile(index=
|
|
3978
|
+
function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3884
3979
|
{
|
|
3885
3980
|
ASSERT(isVector2(index) || typeof index === 'number', 'index must be a vec2 or number');
|
|
3886
3981
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
@@ -3931,8 +4026,8 @@ class TileInfo
|
|
|
3931
4026
|
* @param {Vector2} [pos=vec2()] - Top left corner of tile in pixels
|
|
3932
4027
|
* @param {Vector2} [size] - Size of tile in pixels
|
|
3933
4028
|
* @param {TextureInfo} [textureInfo] - Texture info to use
|
|
3934
|
-
* @param {number} [padding] - How many pixels padding around
|
|
3935
|
-
* @param {number} [bleed] - How many pixels smaller to
|
|
4029
|
+
* @param {number} [padding] - How many pixels padding around all sides of each tile (increases grid size, does not affect tile size)
|
|
4030
|
+
* @param {number} [bleed] - How many pixels smaller to shrink UVS of tiles (does not affect grid size, only UVs)
|
|
3936
4031
|
*/
|
|
3937
4032
|
constructor(pos=vec2(), size=tileDefaultSize, textureInfo=textureInfos[0], padding=tileDefaultPadding, bleed=tileDefaultBleed)
|
|
3938
4033
|
{
|
|
@@ -3964,7 +4059,7 @@ class TileInfo
|
|
|
3964
4059
|
ASSERT(typeof frame === 'number');
|
|
3965
4060
|
const w = this.size.x + this.padding*2;
|
|
3966
4061
|
const x = frame*w;
|
|
3967
|
-
ASSERT(x
|
|
4062
|
+
ASSERT(x + this.size.x <= this.textureInfo.size.x, 'frame extends beyond texture width!');
|
|
3968
4063
|
return this.offset(new Vector2(x));
|
|
3969
4064
|
}
|
|
3970
4065
|
|
|
@@ -4053,7 +4148,7 @@ class TextureInfo
|
|
|
4053
4148
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
4054
4149
|
* @memberof Draw */
|
|
4055
4150
|
function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
4056
|
-
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace, context)
|
|
4151
|
+
angle=0, mirror, additiveColor, useWebGL=glEnable, screenSpace=false, context)
|
|
4057
4152
|
{
|
|
4058
4153
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4059
4154
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -4096,19 +4191,17 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
4096
4191
|
}
|
|
4097
4192
|
else
|
|
4098
4193
|
{
|
|
4099
|
-
//
|
|
4100
|
-
// texture is bound doesn't leak in) and folding color+additive
|
|
4101
|
-
// into the additive slot — matches the Canvas2D path's
|
|
4194
|
+
// untextured: fold color+additive to match the Canvas2D path's
|
|
4102
4195
|
// color.add(additiveColor) on line ~337.
|
|
4103
4196
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
4104
|
-
|
|
4105
|
-
0, combined.rgbaInt());
|
|
4197
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
4106
4198
|
}
|
|
4107
4199
|
}
|
|
4108
4200
|
else
|
|
4109
4201
|
{
|
|
4110
4202
|
// normal canvas 2D rendering method (slower)
|
|
4111
4203
|
++drawCount;
|
|
4204
|
+
++primitiveCount;
|
|
4112
4205
|
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
4113
4206
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
4114
4207
|
{
|
|
@@ -4148,13 +4241,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
4148
4241
|
* @param {Vector2} pos
|
|
4149
4242
|
* @param {Vector2} [size=vec2(1)]
|
|
4150
4243
|
* @param {Color} [colorTop=WHITE]
|
|
4151
|
-
* @param {Color} [colorBottom=
|
|
4244
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
4152
4245
|
* @param {number} [angle]
|
|
4153
4246
|
* @param {boolean} [useWebGL=glEnable]
|
|
4154
4247
|
* @param {boolean} [screenSpace]
|
|
4155
4248
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4156
4249
|
* @memberof Draw */
|
|
4157
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=
|
|
4250
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4158
4251
|
{
|
|
4159
4252
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4160
4253
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -4194,6 +4287,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4194
4287
|
{
|
|
4195
4288
|
// normal canvas 2D rendering method (slower)
|
|
4196
4289
|
++drawCount;
|
|
4290
|
+
++primitiveCount;
|
|
4197
4291
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
4198
4292
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
4199
4293
|
{
|
|
@@ -4256,8 +4350,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
4256
4350
|
return;
|
|
4257
4351
|
}
|
|
4258
4352
|
|
|
4259
|
-
// Canvas2D path — increment
|
|
4353
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
4260
4354
|
++drawCount;
|
|
4355
|
+
++primitiveCount;
|
|
4261
4356
|
|
|
4262
4357
|
if (!screenSpace)
|
|
4263
4358
|
{
|
|
@@ -4310,7 +4405,7 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
4310
4405
|
* @param {boolean} [screenSpace]
|
|
4311
4406
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4312
4407
|
* @memberof Draw */
|
|
4313
|
-
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
4408
|
+
function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4314
4409
|
{
|
|
4315
4410
|
ASSERT(isArray(points), 'points must be an array');
|
|
4316
4411
|
ASSERT(isNumber(width), 'width must be a number');
|
|
@@ -4331,6 +4426,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
4331
4426
|
{
|
|
4332
4427
|
// normal canvas 2D rendering method (slower)
|
|
4333
4428
|
++drawCount;
|
|
4429
|
+
++primitiveCount;
|
|
4334
4430
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
4335
4431
|
{
|
|
4336
4432
|
context.strokeStyle = color.toString();
|
|
@@ -4358,7 +4454,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
4358
4454
|
* @param {boolean} [screenSpace]
|
|
4359
4455
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4360
4456
|
* @memberof Draw */
|
|
4361
|
-
function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, screenSpace, context)
|
|
4457
|
+
function drawLine(posA, posB, width=.1, color=WHITE, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4362
4458
|
{
|
|
4363
4459
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
4364
4460
|
const size = vec2(width, halfDelta.length()*2);
|
|
@@ -4374,9 +4470,9 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
4374
4470
|
* @param {Vector2} [size=vec2(1)]
|
|
4375
4471
|
* @param {number} [sides]
|
|
4376
4472
|
* @param {Color} [color=WHITE]
|
|
4377
|
-
* @param {number} [angle]
|
|
4378
4473
|
* @param {number} [lineWidth]
|
|
4379
4474
|
* @param {Color} [lineColor=BLACK]
|
|
4475
|
+
* @param {number} [angle]
|
|
4380
4476
|
* @param {boolean} [useWebGL=glEnable]
|
|
4381
4477
|
* @param {boolean} [screenSpace]
|
|
4382
4478
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
@@ -4469,7 +4565,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
|
|
|
4469
4565
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4470
4566
|
|
|
4471
4567
|
// clamp line width to prevent artifacts
|
|
4472
|
-
lineWidth = clamp(lineWidth, 0,
|
|
4568
|
+
lineWidth = clamp(lineWidth, 0, min(size.x, size.y));
|
|
4473
4569
|
|
|
4474
4570
|
if (useWebGL && glEnable)
|
|
4475
4571
|
{
|
|
@@ -4511,6 +4607,104 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
4511
4607
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
4512
4608
|
}
|
|
4513
4609
|
|
|
4610
|
+
/** Draw an ellipse filled with a radial gradient from the center to the rim
|
|
4611
|
+
* - Best when batched with other untextured polys
|
|
4612
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
4613
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
4614
|
+
* @param {Vector2} pos
|
|
4615
|
+
* @param {Vector2} [size=vec2(1)] - Width and height diameter
|
|
4616
|
+
* @param {Color} [colorInner=WHITE]
|
|
4617
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
4618
|
+
* @param {number} [angle]
|
|
4619
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
4620
|
+
* @param {boolean} [screenSpace]
|
|
4621
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4622
|
+
* @memberof Draw */
|
|
4623
|
+
let drawEllipseGradientOffset = 0;
|
|
4624
|
+
function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4625
|
+
{
|
|
4626
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4627
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
4628
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
4629
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4630
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4631
|
+
|
|
4632
|
+
if (headlessMode) return;
|
|
4633
|
+
|
|
4634
|
+
if (useWebGL && glEnable)
|
|
4635
|
+
{
|
|
4636
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4637
|
+
if (screenSpace)
|
|
4638
|
+
{
|
|
4639
|
+
// convert to world space
|
|
4640
|
+
pos = screenToWorld(pos);
|
|
4641
|
+
size = size.scale(1/cameraScale);
|
|
4642
|
+
angle += cameraAngle;
|
|
4643
|
+
}
|
|
4644
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
4645
|
+
// so back-to-back gradients at the same position have their hole
|
|
4646
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
4647
|
+
// verts and don't visibly stack
|
|
4648
|
+
const sides = glCircleSides;
|
|
4649
|
+
const radiusX = size.x/2, radiusY = size.y/2;
|
|
4650
|
+
const innerInt = colorInner.rgbaInt();
|
|
4651
|
+
const outerInt = colorOuter.rgbaInt();
|
|
4652
|
+
const offset = drawEllipseGradientOffset++;
|
|
4653
|
+
const c = cos(-angle), s = sin(-angle);
|
|
4654
|
+
const rim = (a) =>
|
|
4655
|
+
{
|
|
4656
|
+
const lx = sin(a)*radiusX, ly = cos(a)*radiusY;
|
|
4657
|
+
return vec2(pos.x + lx*c - ly*s, pos.y + lx*s + ly*c);
|
|
4658
|
+
};
|
|
4659
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
4660
|
+
const points = [rim(startA)];
|
|
4661
|
+
const colors = [outerInt];
|
|
4662
|
+
for (let i=sides; i--;)
|
|
4663
|
+
{
|
|
4664
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
4665
|
+
points.push(pos);
|
|
4666
|
+
colors.push(innerInt);
|
|
4667
|
+
points.push(rim(a));
|
|
4668
|
+
colors.push(outerInt);
|
|
4669
|
+
}
|
|
4670
|
+
glDrawColoredPoints(points, colors);
|
|
4671
|
+
}
|
|
4672
|
+
else
|
|
4673
|
+
{
|
|
4674
|
+
// normal canvas 2D rendering method (slower)
|
|
4675
|
+
++drawCount;
|
|
4676
|
+
++primitiveCount;
|
|
4677
|
+
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
4678
|
+
{
|
|
4679
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
4680
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
4681
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
4682
|
+
context.fillStyle = gradient;
|
|
4683
|
+
context.beginPath();
|
|
4684
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
4685
|
+
context.fill();
|
|
4686
|
+
}, screenSpace, context);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
4691
|
+
* - Best when batched with other untextured polys
|
|
4692
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
4693
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
4694
|
+
* @param {Vector2} pos
|
|
4695
|
+
* @param {number} [size=1] - Diameter
|
|
4696
|
+
* @param {Color} [colorInner=WHITE]
|
|
4697
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
4698
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
4699
|
+
* @param {boolean} [screenSpace]
|
|
4700
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4701
|
+
* @memberof Draw */
|
|
4702
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
4703
|
+
{
|
|
4704
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
4705
|
+
drawEllipseGradient(pos, vec2(size), colorInner, colorOuter, 0, useWebGL, screenSpace, context);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4514
4708
|
/**
|
|
4515
4709
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
4516
4710
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -4565,7 +4759,7 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4565
4759
|
* @param {number} [angle]
|
|
4566
4760
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
4567
4761
|
* @memberof Draw */
|
|
4568
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
4762
|
+
function drawText(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
4569
4763
|
{
|
|
4570
4764
|
// convert to screen space
|
|
4571
4765
|
pos = worldToScreen(pos);
|
|
@@ -4605,16 +4799,16 @@ function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLA
|
|
|
4605
4799
|
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
4606
4800
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4607
4801
|
|
|
4802
|
+
const lines = (text+'').split('\n');
|
|
4803
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4804
|
+
// save before style mutations so caller's context state is preserved
|
|
4805
|
+
context.save();
|
|
4608
4806
|
context.fillStyle = color.toString();
|
|
4609
4807
|
context.strokeStyle = lineColor.toString();
|
|
4610
4808
|
context.lineWidth = lineWidth;
|
|
4611
4809
|
context.textAlign = textAlign;
|
|
4612
4810
|
context.font = fontStyle + ' ' + size + 'px '+ font;
|
|
4613
4811
|
context.textBaseline = 'middle';
|
|
4614
|
-
|
|
4615
|
-
const lines = (text+'').split('\n');
|
|
4616
|
-
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4617
|
-
context.save();
|
|
4618
4812
|
context.translate(pos.x, posY);
|
|
4619
4813
|
context.rotate(-angle);
|
|
4620
4814
|
let yOffset = 0;
|
|
@@ -4775,6 +4969,9 @@ function isOnScreen(pos, size=0)
|
|
|
4775
4969
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4776
4970
|
ASSERT(isVector2(size) || isNumber(size), 'size must be a vec2 or number');
|
|
4777
4971
|
|
|
4972
|
+
// cameraScale of 0 collapses world coords; nothing is visible
|
|
4973
|
+
if (!cameraScale) return false;
|
|
4974
|
+
|
|
4778
4975
|
// optimized circle on screen test
|
|
4779
4976
|
// pos = worldToScreen(pos);
|
|
4780
4977
|
let x = pos.x - cameraPos.x;
|
|
@@ -4816,7 +5013,10 @@ function combineCanvases()
|
|
|
4816
5013
|
const w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
4817
5014
|
workCanvas.width = w;
|
|
4818
5015
|
workCanvas.height = h;
|
|
4819
|
-
|
|
5016
|
+
// remove background alpha — explicit fillStyle so a previous caller
|
|
5017
|
+
// leaving workContext.fillStyle transparent can't silently no-op this
|
|
5018
|
+
workContext.fillStyle = '#000';
|
|
5019
|
+
workContext.fillRect(0,0,w,h);
|
|
4820
5020
|
glCopyToContext(workContext);
|
|
4821
5021
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
4822
5022
|
mainContext.drawImage(workCanvas, 0, 0);
|
|
@@ -4959,33 +5159,33 @@ function setCursor(cursorStyle = 'auto')
|
|
|
4959
5159
|
///////////////////////////////////////////////////////////////////////////////
|
|
4960
5160
|
|
|
4961
5161
|
/** Engine font image, 8x8 font provided by the engine
|
|
4962
|
-
* @type {
|
|
5162
|
+
* @type {ImageFont}
|
|
4963
5163
|
* @memberof Draw */
|
|
4964
|
-
let
|
|
5164
|
+
let engineImageFont;
|
|
4965
5165
|
|
|
4966
5166
|
/**
|
|
4967
|
-
* Font
|
|
5167
|
+
* Image Font Object - Draw text by using tiles in an image
|
|
4968
5168
|
* - 96 characters (from space to tilde) are stored in an image
|
|
4969
5169
|
* - A 8x8 default engine font is supplied for general use
|
|
4970
5170
|
* - This system is WebGL enabled for fast text rendering
|
|
4971
5171
|
* - Fonts can also be colored and scaled along each axis
|
|
4972
|
-
*
|
|
5172
|
+
*
|
|
4973
5173
|
* @memberof Draw
|
|
4974
5174
|
* @example
|
|
4975
5175
|
* // use built in font
|
|
4976
|
-
* const font =
|
|
5176
|
+
* const font = engineImageFont;
|
|
4977
5177
|
*
|
|
4978
5178
|
* // draw text
|
|
4979
5179
|
* font.drawTextScreen('LittleJS\nHello World!', vec2(200, 50));
|
|
4980
5180
|
*/
|
|
4981
|
-
class
|
|
5181
|
+
class ImageFont
|
|
4982
5182
|
{
|
|
4983
5183
|
/** Create an image font
|
|
4984
5184
|
* @param {TileInfo} tileInfo - Tile info of first character in font
|
|
4985
5185
|
*/
|
|
4986
5186
|
constructor(tileInfo)
|
|
4987
5187
|
{
|
|
4988
|
-
ASSERT(!!tileInfo, 'tileInfo is required for
|
|
5188
|
+
ASSERT(!!tileInfo, 'tileInfo is required for ImageFont');
|
|
4989
5189
|
|
|
4990
5190
|
/** @property {TileInfo} - Tile info for the font */
|
|
4991
5191
|
this.tileInfo = tileInfo.frame(0);
|
|
@@ -5070,7 +5270,7 @@ class FontImage
|
|
|
5070
5270
|
}
|
|
5071
5271
|
|
|
5072
5272
|
// load engine font, called automatically on startup
|
|
5073
|
-
async function
|
|
5273
|
+
async function imageFontInit()
|
|
5074
5274
|
{
|
|
5075
5275
|
const image = new Image;
|
|
5076
5276
|
await new Promise(resolve =>
|
|
@@ -5083,7 +5283,7 @@ async function fontImageInit()
|
|
|
5083
5283
|
const tilePos=vec2(), tileSize=vec2(8), padding=1, bleed=0;
|
|
5084
5284
|
const textureInfo = new TextureInfo(image);
|
|
5085
5285
|
const tileInfo = new TileInfo(tilePos, tileSize, textureInfo, padding, bleed);
|
|
5086
|
-
|
|
5286
|
+
engineImageFont = new ImageFont(tileInfo);
|
|
5087
5287
|
}
|
|
5088
5288
|
/**
|
|
5089
5289
|
* LittleJS Input System
|
|
@@ -5532,9 +5732,11 @@ function inputInit()
|
|
|
5532
5732
|
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
5533
5733
|
}
|
|
5534
5734
|
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
5535
|
-
function onMouseWheel(e)
|
|
5536
|
-
{
|
|
5537
|
-
|
|
5735
|
+
function onMouseWheel(e)
|
|
5736
|
+
{
|
|
5737
|
+
// accumulate so multiple wheel events in one frame are not lost
|
|
5738
|
+
if (!e.ctrlKey)
|
|
5739
|
+
mouseWheel += sign(e.deltaY);
|
|
5538
5740
|
if (inputPreventDefault && e.cancelable && document.hasFocus())
|
|
5539
5741
|
e.preventDefault(); // prevent page scrolling
|
|
5540
5742
|
}
|
|
@@ -5659,9 +5861,14 @@ function inputInit()
|
|
|
5659
5861
|
if (button < touchGamepadButtonCount)
|
|
5660
5862
|
touchGamepadButtons[button] = 1;
|
|
5661
5863
|
}
|
|
5662
|
-
else if (startCenter.distance(touchPos)
|
|
5864
|
+
else if (startCenter.distance(touchPos) < touchGamepadCenterButtonSize &&
|
|
5865
|
+
stickCenter.distance(touchPos) >= 2 * touchGamepadSize &&
|
|
5866
|
+
buttonCenter.distance(touchPos) >= 2 * touchGamepadSize)
|
|
5663
5867
|
{
|
|
5664
5868
|
// virtual start button in center
|
|
5869
|
+
// require a fat-finger buffer of touchGamepadSize beyond the
|
|
5870
|
+
// edge of the stick/buttons so drift off those controls can't
|
|
5871
|
+
// accidentally fire start
|
|
5665
5872
|
touchGamepadButtons[9] = 1;
|
|
5666
5873
|
}
|
|
5667
5874
|
}
|
|
@@ -5703,7 +5910,7 @@ function inputUpdate()
|
|
|
5703
5910
|
v > min ? percent(v, min, max) :
|
|
5704
5911
|
v < -min ? -percent(-v, min, max) : 0;
|
|
5705
5912
|
return vec2(deadZone(v.x), deadZone(-v.y)).clampLength();
|
|
5706
|
-
}
|
|
5913
|
+
};
|
|
5707
5914
|
|
|
5708
5915
|
// update touch gamepad if enabled
|
|
5709
5916
|
if (touchGamepadEnable && isTouchDevice)
|
|
@@ -5717,7 +5924,12 @@ function inputUpdate()
|
|
|
5717
5924
|
debugCircle(stickCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5718
5925
|
debugCircle(buttonCenter, 2*touchGamepadSize, 'cyan', 0, false, true);
|
|
5719
5926
|
if (touchGamepadCenterButtonSize)
|
|
5927
|
+
{
|
|
5720
5928
|
debugCircle(startCenter, 2*touchGamepadCenterButtonSize, 'cyan', 0, false, true);
|
|
5929
|
+
// exclusion bubbles around controls (where start is blocked)
|
|
5930
|
+
debugCircle(stickCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5931
|
+
debugCircle(buttonCenter, 4*touchGamepadSize, 'magenta', 0, false, true);
|
|
5932
|
+
}
|
|
5721
5933
|
}
|
|
5722
5934
|
|
|
5723
5935
|
if (!touchGamepadTimer.isSet()) return;
|
|
@@ -5824,13 +6036,6 @@ function inputUpdate()
|
|
|
5824
6036
|
(gamepadIsDown(15,i)&&1) - (gamepadIsDown(14,i)&&1),
|
|
5825
6037
|
(gamepadIsDown(12,i)&&1) - (gamepadIsDown(13,i)&&1));
|
|
5826
6038
|
}
|
|
5827
|
-
else if (gamepad.axes && gamepad.axes.length >= 2)
|
|
5828
|
-
{
|
|
5829
|
-
// digital style dpad from axes
|
|
5830
|
-
const x = clamp(round(gamepad.axes[0]), -1, 1);
|
|
5831
|
-
const y = clamp(round(gamepad.axes[1]), -1, 1);
|
|
5832
|
-
dpad.set(x, -y);
|
|
5833
|
-
}
|
|
5834
6039
|
|
|
5835
6040
|
// copy dpad to left analog stick when pressed
|
|
5836
6041
|
if (gamepadDirectionEmulateStick && (dpad.x || dpad.y))
|
|
@@ -6036,7 +6241,7 @@ class Sound
|
|
|
6036
6241
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
6037
6242
|
this.onloadCallback = onloadCallback;
|
|
6038
6243
|
|
|
6039
|
-
if (
|
|
6244
|
+
if (isArray(asset))
|
|
6040
6245
|
{
|
|
6041
6246
|
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
6042
6247
|
const zzfxSound = asset.slice();
|
|
@@ -6128,7 +6333,7 @@ class Sound
|
|
|
6128
6333
|
}
|
|
6129
6334
|
|
|
6130
6335
|
/** Get how long this sound is in seconds
|
|
6131
|
-
* @return {number} - How long the sound is in seconds (
|
|
6336
|
+
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
6132
6337
|
*/
|
|
6133
6338
|
getDuration()
|
|
6134
6339
|
{ return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
|
|
@@ -6285,10 +6490,14 @@ class SoundInstance
|
|
|
6285
6490
|
{
|
|
6286
6491
|
if (fadeTime)
|
|
6287
6492
|
{
|
|
6288
|
-
// ramp off gain
|
|
6493
|
+
// ramp off gain from current volume (not 1, or low-volume
|
|
6494
|
+
// instances would jump back up before fading);
|
|
6495
|
+
// cancel any prior scheduling so stacked stop calls don't
|
|
6496
|
+
// re-anchor partway through a previous fade
|
|
6289
6497
|
const startFade = audioContext.currentTime;
|
|
6290
6498
|
const endFade = startFade + fadeTime;
|
|
6291
|
-
this.gainNode.gain.
|
|
6499
|
+
this.gainNode.gain.cancelScheduledValues(startFade);
|
|
6500
|
+
this.gainNode.gain.setValueAtTime(this.volume, startFade);
|
|
6292
6501
|
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
6293
6502
|
this.source.stop(endFade);
|
|
6294
6503
|
}
|
|
@@ -6336,13 +6545,14 @@ class SoundInstance
|
|
|
6336
6545
|
*/
|
|
6337
6546
|
getCurrentTime()
|
|
6338
6547
|
{
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6548
|
+
if (!this.isPlaying()) return this.pausedTime;
|
|
6549
|
+
const duration = this.getDuration();
|
|
6550
|
+
// guard mod against 0 duration (rate=0 or sound not loaded)
|
|
6551
|
+
return duration ? mod(audioContext.currentTime - this.startTime, duration) : 0;
|
|
6342
6552
|
}
|
|
6343
6553
|
|
|
6344
6554
|
/** Get the total duration of this sound
|
|
6345
|
-
* @return {number} - Total duration in seconds
|
|
6555
|
+
* @return {number} - Total duration in seconds (0 if loading)
|
|
6346
6556
|
*/
|
|
6347
6557
|
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
6348
6558
|
|
|
@@ -6356,16 +6566,17 @@ class SoundInstance
|
|
|
6356
6566
|
|
|
6357
6567
|
/** Speak text with passed in settings
|
|
6358
6568
|
* @param {string} text - The text to speak
|
|
6359
|
-
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
6360
6569
|
* @param {number} [volume] - How much to scale volume by
|
|
6361
6570
|
* @param {number} [rate] - How quickly to speak
|
|
6362
6571
|
* @param {number} [pitch] - How much to change the pitch by
|
|
6572
|
+
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
6363
6573
|
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
6364
6574
|
* @memberof Audio */
|
|
6365
|
-
function speak(text,
|
|
6575
|
+
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
6366
6576
|
{
|
|
6577
|
+
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
6367
6578
|
if (!soundEnable || headlessMode) return;
|
|
6368
|
-
if (
|
|
6579
|
+
if (typeof speechSynthesis === 'undefined') return;
|
|
6369
6580
|
|
|
6370
6581
|
// common languages (not supported by all browsers)
|
|
6371
6582
|
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
@@ -6383,7 +6594,11 @@ function speak(text, language='', volume=1, rate=1, pitch=1)
|
|
|
6383
6594
|
|
|
6384
6595
|
/** Stop all queued speech
|
|
6385
6596
|
* @memberof Audio */
|
|
6386
|
-
function speakStop()
|
|
6597
|
+
function speakStop()
|
|
6598
|
+
{
|
|
6599
|
+
if (typeof speechSynthesis !== 'undefined')
|
|
6600
|
+
speechSynthesis.cancel();
|
|
6601
|
+
}
|
|
6387
6602
|
|
|
6388
6603
|
/** Get frequency of a note on a musical scale
|
|
6389
6604
|
* @param {number} semitoneOffset - How many semitones away from the root note
|
|
@@ -6445,9 +6660,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
6445
6660
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
6446
6661
|
source.connect(pannerNode).connect(gainNode);
|
|
6447
6662
|
|
|
6448
|
-
//
|
|
6449
|
-
|
|
6450
|
-
|
|
6663
|
+
// disconnect nodes when the sound ends so the audio graph doesn't grow
|
|
6664
|
+
// unbounded across many play() calls (source.stop() also fires 'ended')
|
|
6665
|
+
source.addEventListener('ended', ()=>
|
|
6666
|
+
{
|
|
6667
|
+
gainNode.disconnect();
|
|
6668
|
+
pannerNode.disconnect();
|
|
6669
|
+
if (onended) onended(source);
|
|
6670
|
+
});
|
|
6451
6671
|
|
|
6452
6672
|
// play and return sound
|
|
6453
6673
|
const startOffset = offset * rate;
|
|
@@ -6684,14 +6904,29 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
|
6684
6904
|
* @memberof TileLayers */
|
|
6685
6905
|
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
6686
6906
|
{
|
|
6907
|
+
// check every layer and keep the closest hit so a far hit in an
|
|
6908
|
+
// earlier-registered layer doesn't shadow a closer hit in a later one
|
|
6909
|
+
let closestHit, closestDistSq, closestNormal;
|
|
6910
|
+
const scratchNormal = normal && vec2();
|
|
6687
6911
|
for (const layer of tileCollisionLayers)
|
|
6688
6912
|
{
|
|
6689
6913
|
if (!solidOnly || layer.isSolid)
|
|
6690
6914
|
{
|
|
6691
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject,
|
|
6692
|
-
if (hitPos)
|
|
6915
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, scratchNormal);
|
|
6916
|
+
if (hitPos)
|
|
6917
|
+
{
|
|
6918
|
+
const d = posStart.distanceSquared(hitPos);
|
|
6919
|
+
if (closestHit === undefined || d < closestDistSq)
|
|
6920
|
+
{
|
|
6921
|
+
closestHit = hitPos;
|
|
6922
|
+
closestDistSq = d;
|
|
6923
|
+
if (normal) closestNormal = scratchNormal.copy();
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6693
6926
|
}
|
|
6694
6927
|
}
|
|
6928
|
+
if (closestHit && normal) normal.setFrom(closestNormal);
|
|
6929
|
+
return closestHit;
|
|
6695
6930
|
}
|
|
6696
6931
|
|
|
6697
6932
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6865,38 +7100,6 @@ class CanvasLayer extends EngineObject
|
|
|
6865
7100
|
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
6866
7101
|
}
|
|
6867
7102
|
|
|
6868
|
-
/** Draw a tile onto the layer canvas in world space
|
|
6869
|
-
* @param {Vector2} pos
|
|
6870
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6871
|
-
* @param {TileInfo} [tileInfo]
|
|
6872
|
-
* @param {Color} [color=WHITE]
|
|
6873
|
-
* @param {number} [angle]
|
|
6874
|
-
* @param {boolean} [mirror] */
|
|
6875
|
-
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
6876
|
-
{
|
|
6877
|
-
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
6878
|
-
size = size.multiply(this.tileInfo.size);
|
|
6879
|
-
pos.y = this.canvas.height - pos.y;
|
|
6880
|
-
|
|
6881
|
-
// draw the tile onto the layer canvas
|
|
6882
|
-
const oldMainCanvasSize = mainCanvasSize;
|
|
6883
|
-
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
6884
|
-
const useWebGL = this.hasWebGL();
|
|
6885
|
-
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
6886
|
-
const drawContext = useWebGL ? undefined : this.context;
|
|
6887
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
6888
|
-
useWebGL && glSetRenderTarget();
|
|
6889
|
-
mainCanvasSize = oldMainCanvasSize;
|
|
6890
|
-
}
|
|
6891
|
-
|
|
6892
|
-
/** Draw a rectangle onto the layer canvas in world space
|
|
6893
|
-
* @param {Vector2} pos
|
|
6894
|
-
* @param {Vector2} [size=vec2(1)]
|
|
6895
|
-
* @param {Color} [color=WHITE]
|
|
6896
|
-
* @param {number} [angle] */
|
|
6897
|
-
drawRect(pos, size, color, angle)
|
|
6898
|
-
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
6899
|
-
|
|
6900
7103
|
/** Create WebGL texture if necessary and copy layer canvas to it */
|
|
6901
7104
|
updateWebGL()
|
|
6902
7105
|
{ this.textureInfo.createWebGLTexture(); }
|
|
@@ -6952,6 +7155,8 @@ class TileLayer extends CanvasLayer
|
|
|
6952
7155
|
this.redrawTileData = ()=> {};
|
|
6953
7156
|
this.drawLayerTile = ()=> {};
|
|
6954
7157
|
this.drawLayerRect = ()=> {};
|
|
7158
|
+
this.drawTile = ()=> {};
|
|
7159
|
+
this.drawRect = ()=> {};
|
|
6955
7160
|
this.clearLayerRect = ()=> {};
|
|
6956
7161
|
return;
|
|
6957
7162
|
}
|
|
@@ -6979,7 +7184,7 @@ class TileLayer extends CanvasLayer
|
|
|
6979
7184
|
ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
|
|
6980
7185
|
|
|
6981
7186
|
if (!layerPos.arrayCheck(this.size)) return;
|
|
6982
|
-
this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
|
|
7187
|
+
this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] = data;
|
|
6983
7188
|
|
|
6984
7189
|
if (!redraw) return;
|
|
6985
7190
|
const isRedraw = drawContext === this.context;
|
|
@@ -6994,11 +7199,11 @@ class TileLayer extends CanvasLayer
|
|
|
6994
7199
|
|
|
6995
7200
|
/** Get data at a given position in the array
|
|
6996
7201
|
* @param {Vector2} layerPos - Local position in array
|
|
6997
|
-
* @return {TileLayerData} */
|
|
7202
|
+
* @return {TileLayerData|undefined} */
|
|
6998
7203
|
getData(layerPos)
|
|
6999
|
-
{
|
|
7204
|
+
{
|
|
7000
7205
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
7001
|
-
return layerPos.arrayCheck(this.size)
|
|
7206
|
+
return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
|
|
7002
7207
|
}
|
|
7003
7208
|
|
|
7004
7209
|
// Update the tile layer, refresh texture if needed
|
|
@@ -7106,7 +7311,7 @@ class TileLayer extends CanvasLayer
|
|
|
7106
7311
|
|
|
7107
7312
|
// draw the tile if it has layer data
|
|
7108
7313
|
const d = this.getData(layerPos);
|
|
7109
|
-
if (!d.tile) return;
|
|
7314
|
+
if (!d || !d.tile) return;
|
|
7110
7315
|
|
|
7111
7316
|
const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
|
|
7112
7317
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -7152,6 +7357,38 @@ class TileLayer extends CanvasLayer
|
|
|
7152
7357
|
drawLayerRect(pos, size, color, angle=0)
|
|
7153
7358
|
{ this.drawLayerTile(pos, size, undefined, color, angle); }
|
|
7154
7359
|
|
|
7360
|
+
/** Draw a tile onto the layer canvas in world space
|
|
7361
|
+
* @param {Vector2} pos
|
|
7362
|
+
* @param {Vector2} [size=vec2(1)]
|
|
7363
|
+
* @param {TileInfo} [tileInfo]
|
|
7364
|
+
* @param {Color} [color=WHITE]
|
|
7365
|
+
* @param {number} [angle]
|
|
7366
|
+
* @param {boolean} [mirror] */
|
|
7367
|
+
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
7368
|
+
{
|
|
7369
|
+
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
7370
|
+
size = size.multiply(this.tileInfo.size);
|
|
7371
|
+
pos.y = this.canvas.height - pos.y;
|
|
7372
|
+
|
|
7373
|
+
// draw the tile onto the layer canvas
|
|
7374
|
+
const oldMainCanvasSize = mainCanvasSize;
|
|
7375
|
+
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
7376
|
+
const useWebGL = this.hasWebGL();
|
|
7377
|
+
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
7378
|
+
const drawContext = useWebGL ? undefined : this.context;
|
|
7379
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
7380
|
+
useWebGL && glSetRenderTarget();
|
|
7381
|
+
mainCanvasSize = oldMainCanvasSize;
|
|
7382
|
+
}
|
|
7383
|
+
|
|
7384
|
+
/** Draw a rectangle onto the layer canvas in world space
|
|
7385
|
+
* @param {Vector2} pos
|
|
7386
|
+
* @param {Vector2} [size=vec2(1)]
|
|
7387
|
+
* @param {Color} [color=WHITE]
|
|
7388
|
+
* @param {number} [angle] */
|
|
7389
|
+
drawRect(pos, size, color, angle)
|
|
7390
|
+
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
7391
|
+
|
|
7155
7392
|
/** Clear a rectangle in layer space
|
|
7156
7393
|
* @param {Vector2} pos - position in pixel coordinates
|
|
7157
7394
|
* @param {Vector2} size
|
|
@@ -7230,7 +7467,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
7230
7467
|
setCollisionData(layerPos, data=1)
|
|
7231
7468
|
{
|
|
7232
7469
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
7233
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
7470
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
7234
7471
|
layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
|
|
7235
7472
|
}
|
|
7236
7473
|
|
|
@@ -7245,7 +7482,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
7245
7482
|
getCollisionData(layerPos)
|
|
7246
7483
|
{
|
|
7247
7484
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
7248
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
7485
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
7249
7486
|
return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
|
|
7250
7487
|
}
|
|
7251
7488
|
|
|
@@ -7270,8 +7507,10 @@ class TileCollisionLayer extends TileLayer
|
|
|
7270
7507
|
const posY = pos.y - this.pos.y;
|
|
7271
7508
|
const minX = max(posX - size.x/2|0, 0);
|
|
7272
7509
|
const minY = max(posY - size.y/2|0, 0);
|
|
7273
|
-
|
|
7274
|
-
|
|
7510
|
+
// ensure at least one cell is visited even when size is 0 and pos
|
|
7511
|
+
// lands exactly on an integer boundary (documented point-test mode)
|
|
7512
|
+
const maxX = min(max(posX + size.x/2, minX + 1), this.size.x);
|
|
7513
|
+
const maxY = min(max(posY + size.y/2, minY + 1), this.size.y);
|
|
7275
7514
|
const hitPos = new Vector2;
|
|
7276
7515
|
for (let y = minY; y < maxY; ++y)
|
|
7277
7516
|
for (let x = minX; x < maxX; ++x)
|
|
@@ -7390,7 +7629,7 @@ class ParticleEmitter extends EngineObject
|
|
|
7390
7629
|
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
7391
7630
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
7392
7631
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
7393
|
-
* @param {number} [fadeRate] -
|
|
7632
|
+
* @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
|
|
7394
7633
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
7395
7634
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
7396
7635
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
@@ -7474,7 +7713,7 @@ class ParticleEmitter extends EngineObject
|
|
|
7474
7713
|
this.gravityScale = gravityScale;
|
|
7475
7714
|
/** @property {number} - Cone for start particle angle */
|
|
7476
7715
|
this.particleConeAngle = particleConeAngle;
|
|
7477
|
-
/** @property {number} -
|
|
7716
|
+
/** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
|
|
7478
7717
|
this.fadeRate = fadeRate;
|
|
7479
7718
|
/** @property {number} - Apply extra randomness percent */
|
|
7480
7719
|
this.randomness = randomness;
|
|
@@ -7752,33 +7991,32 @@ class Particle
|
|
|
7752
7991
|
const hitLayer = tileCollisionTest(this.pos);
|
|
7753
7992
|
if (!testCollision(oldPos))
|
|
7754
7993
|
{
|
|
7755
|
-
|
|
7994
|
+
// testCollision already invoked collideCallback with the
|
|
7995
|
+
// correct (this, data, pos) args; no need to re-check here.
|
|
7996
|
+
// test which side we bounced off (or both if a corner)
|
|
7997
|
+
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
7998
|
+
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
7999
|
+
const hitRestitution = max(restitution, hitLayer.restitution);
|
|
8000
|
+
const hitFriction = max(friction, hitLayer.friction);
|
|
8001
|
+
if (isBlockedX)
|
|
7756
8002
|
{
|
|
7757
|
-
//
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
const hitFriction = max(friction, hitLayer.friction);
|
|
7762
|
-
if (isBlockedX)
|
|
7763
|
-
{
|
|
7764
|
-
// move to previous X position and bounce
|
|
7765
|
-
this.pos.x = oldPos.x;
|
|
7766
|
-
this.velocity.x *= -hitRestitution;
|
|
7767
|
-
this.velocity.y *= hitFriction;
|
|
7768
|
-
}
|
|
7769
|
-
if (isBlockedY || !isBlockedX)
|
|
7770
|
-
{
|
|
7771
|
-
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
7772
|
-
if (wasFalling)
|
|
7773
|
-
this.groundObject = hitLayer;
|
|
7774
|
-
|
|
7775
|
-
// move to previous Y position and bounce
|
|
7776
|
-
this.pos.y = oldPos.y;
|
|
7777
|
-
this.velocity.y *= -hitRestitution;
|
|
7778
|
-
this.velocity.x *= hitFriction;
|
|
7779
|
-
}
|
|
7780
|
-
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
8003
|
+
// move to previous X position and bounce
|
|
8004
|
+
this.pos.x = oldPos.x;
|
|
8005
|
+
this.velocity.x *= -hitRestitution;
|
|
8006
|
+
this.velocity.y *= hitFriction;
|
|
7781
8007
|
}
|
|
8008
|
+
if (isBlockedY || !isBlockedX)
|
|
8009
|
+
{
|
|
8010
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
8011
|
+
if (wasFalling)
|
|
8012
|
+
this.groundObject = hitLayer;
|
|
8013
|
+
|
|
8014
|
+
// move to previous Y position and bounce
|
|
8015
|
+
this.pos.y = oldPos.y;
|
|
8016
|
+
this.velocity.y *= -hitRestitution;
|
|
8017
|
+
this.velocity.x *= hitFriction;
|
|
8018
|
+
}
|
|
8019
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
7782
8020
|
}
|
|
7783
8021
|
}
|
|
7784
8022
|
}
|
|
@@ -7939,6 +8177,10 @@ function glInit(rootElement)
|
|
|
7939
8177
|
for (const info of glTextureInfos)
|
|
7940
8178
|
info.glTexture = undefined;
|
|
7941
8179
|
glActiveTexture = undefined;
|
|
8180
|
+
// drop any partially-filled batch so the next glFlush doesn't
|
|
8181
|
+
// upload stale glBatchCount against fresh empty buffers on restore
|
|
8182
|
+
glBatchCount = 0;
|
|
8183
|
+
glPolyMode = false;
|
|
7942
8184
|
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
7943
8185
|
});
|
|
7944
8186
|
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
@@ -8297,6 +8539,10 @@ function glSetTextureData(texture, image)
|
|
|
8297
8539
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
8298
8540
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
8299
8541
|
|
|
8542
|
+
// keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
|
|
8543
|
+
if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
|
|
8544
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
8545
|
+
|
|
8300
8546
|
// rebind active texture
|
|
8301
8547
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
8302
8548
|
}
|
|
@@ -8342,7 +8588,7 @@ function glFlush()
|
|
|
8342
8588
|
{
|
|
8343
8589
|
if (glEnable && glContext && glBatchCount)
|
|
8344
8590
|
{
|
|
8345
|
-
// set
|
|
8591
|
+
// set blend mode
|
|
8346
8592
|
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
8347
8593
|
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
8348
8594
|
glContext.enable(glContext.BLEND);
|
|
@@ -8356,7 +8602,8 @@ function glFlush()
|
|
|
8356
8602
|
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
8357
8603
|
else
|
|
8358
8604
|
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
8359
|
-
drawCount
|
|
8605
|
+
++drawCount;
|
|
8606
|
+
primitiveCount += glBatchCount;
|
|
8360
8607
|
glBatchCount = 0;
|
|
8361
8608
|
}
|
|
8362
8609
|
glBatchAdditive = glAdditive;
|
|
@@ -8417,6 +8664,22 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
8417
8664
|
glPositionData[offset++] = angle;
|
|
8418
8665
|
}
|
|
8419
8666
|
|
|
8667
|
+
/** Add an untextured rect to the gl draw list
|
|
8668
|
+
* Zeroes the uvs and rgba so the texture contribution multiplies to 0,
|
|
8669
|
+
* then carries the real color in the additive slot. Works regardless of
|
|
8670
|
+
* which texture is currently bound.
|
|
8671
|
+
* @param {number} x
|
|
8672
|
+
* @param {number} y
|
|
8673
|
+
* @param {number} sizeX
|
|
8674
|
+
* @param {number} sizeY
|
|
8675
|
+
* @param {number} angle
|
|
8676
|
+
* @param {number} rgba - color as 32-bit integer
|
|
8677
|
+
* @memberof WebGL */
|
|
8678
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
8679
|
+
{
|
|
8680
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
8681
|
+
}
|
|
8682
|
+
|
|
8420
8683
|
/** Transform and add a polygon to the gl draw list
|
|
8421
8684
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
8422
8685
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -8430,13 +8693,13 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
8430
8693
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
8431
8694
|
{
|
|
8432
8695
|
const pointsOut = [];
|
|
8696
|
+
const sa = sin(-angle);
|
|
8697
|
+
const ca = cos(-angle);
|
|
8433
8698
|
for (const p of points)
|
|
8434
8699
|
{
|
|
8435
8700
|
// transform the point
|
|
8436
8701
|
const px = p.x*sx;
|
|
8437
8702
|
const py = p.y*sy;
|
|
8438
|
-
const sa = sin(-angle);
|
|
8439
|
-
const ca = cos(-angle);
|
|
8440
8703
|
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
8441
8704
|
}
|
|
8442
8705
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
@@ -8468,11 +8731,13 @@ function glDrawPoints(points, rgba)
|
|
|
8468
8731
|
{
|
|
8469
8732
|
if (!glEnable || points.length < 3)
|
|
8470
8733
|
return; // needs at least 3 points to have area
|
|
8471
|
-
|
|
8734
|
+
|
|
8472
8735
|
// flush if there is not enough room or if different blend mode
|
|
8473
8736
|
const vertCount = points.length + 2;
|
|
8474
8737
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8475
8738
|
glFlush();
|
|
8739
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
8740
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
8476
8741
|
glSetPolyMode();
|
|
8477
8742
|
|
|
8478
8743
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -8496,11 +8761,13 @@ function glDrawColoredPoints(points, pointColors)
|
|
|
8496
8761
|
{
|
|
8497
8762
|
if (!glEnable || points.length < 3)
|
|
8498
8763
|
return; // needs at least 3 points to have area
|
|
8499
|
-
|
|
8764
|
+
|
|
8500
8765
|
// flush if there is not enough room or if different blend mode
|
|
8501
8766
|
const vertCount = points.length + 2;
|
|
8502
8767
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8503
8768
|
glFlush();
|
|
8769
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
8770
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
8504
8771
|
glSetPolyMode();
|
|
8505
8772
|
|
|
8506
8773
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -8570,7 +8837,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
8570
8837
|
const strip = [];
|
|
8571
8838
|
const n = points.length;
|
|
8572
8839
|
const e = 1e-6;
|
|
8573
|
-
|
|
8840
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
8841
|
+
const miterLimit = 10;
|
|
8574
8842
|
for (let i = 0; i < n; i++)
|
|
8575
8843
|
{
|
|
8576
8844
|
// for each vertex, calculate normal based on adjacent edges
|
|
@@ -8826,7 +9094,7 @@ function drawEngineLogo(t)
|
|
|
8826
9094
|
x.closePath();
|
|
8827
9095
|
gradient(0, Y, 0, Y+H,C);
|
|
8828
9096
|
}
|
|
8829
|
-
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}
|
|
9097
|
+
const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%)`:'#000';
|
|
8830
9098
|
|
|
8831
9099
|
// center and fit to screen
|
|
8832
9100
|
const alpha = oscillate(1,1,t);
|
|
@@ -9053,7 +9321,8 @@ class Medal
|
|
|
9053
9321
|
/** @property {boolean} - Is the medal unlocked? */
|
|
9054
9322
|
this.unlocked = false;
|
|
9055
9323
|
|
|
9056
|
-
|
|
9324
|
+
/** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
|
|
9325
|
+
this.image = undefined;
|
|
9057
9326
|
if (src)
|
|
9058
9327
|
(this.image = new Image).src = src;
|
|
9059
9328
|
|
|
@@ -9216,13 +9485,18 @@ class NewgroundsPlugin
|
|
|
9216
9485
|
ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
|
|
9217
9486
|
|
|
9218
9487
|
newgrounds = this; // set global newgrounds object
|
|
9488
|
+
/** @property {string} - The newgrounds App ID */
|
|
9219
9489
|
this.app_id = app_id;
|
|
9490
|
+
/** @property {string|undefined} - AES-128/Base64 encryption key, if any */
|
|
9220
9491
|
this.cipher = cipher;
|
|
9492
|
+
/** @property {Object|undefined} - CryptoJS instance used when cipher is set */
|
|
9221
9493
|
this.cryptoJS = cryptoJS;
|
|
9494
|
+
/** @property {string} - Hostname used when logging views */
|
|
9222
9495
|
this.host = location ? location.hostname : '';
|
|
9223
9496
|
|
|
9224
9497
|
// get session id from url search params
|
|
9225
9498
|
const url = new URL(location.href);
|
|
9499
|
+
/** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
|
|
9226
9500
|
this.session_id = url.searchParams.get('ngio_session_id');
|
|
9227
9501
|
|
|
9228
9502
|
if (!this.session_id)
|
|
@@ -9230,7 +9504,20 @@ class NewgroundsPlugin
|
|
|
9230
9504
|
|
|
9231
9505
|
// get medals
|
|
9232
9506
|
const medalsResult = this.call('Medal.getList');
|
|
9233
|
-
|
|
9507
|
+
|
|
9508
|
+
// bail early if the first call failed (offline / bad session /
|
|
9509
|
+
// server error) so we don't block the main thread on more sync
|
|
9510
|
+
// XHRs that are guaranteed to also fail
|
|
9511
|
+
if (!medalsResult || !medalsResult.result || medalsResult.result.error)
|
|
9512
|
+
{
|
|
9513
|
+
debugMedals && LOG('Newgrounds session unavailable; skipping plugin init');
|
|
9514
|
+
this.medals = [];
|
|
9515
|
+
this.scoreboards = [];
|
|
9516
|
+
return;
|
|
9517
|
+
}
|
|
9518
|
+
|
|
9519
|
+
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
9520
|
+
this.medals = medalsResult.result.data?.['medals'] || [];
|
|
9234
9521
|
debugMedals && LOG(this.medals);
|
|
9235
9522
|
for (const newgroundsMedal of this.medals)
|
|
9236
9523
|
{
|
|
@@ -9250,10 +9537,11 @@ class NewgroundsPlugin
|
|
|
9250
9537
|
medal.description = medal.description + ` (${ medal.value })`;
|
|
9251
9538
|
}
|
|
9252
9539
|
}
|
|
9253
|
-
|
|
9540
|
+
|
|
9254
9541
|
// get scoreboards
|
|
9255
9542
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
9256
|
-
|
|
9543
|
+
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
9544
|
+
this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
|
|
9257
9545
|
debugMedals && LOG(this.scoreboards);
|
|
9258
9546
|
|
|
9259
9547
|
// keep the session alive with a ping every minute
|
|
@@ -9437,10 +9725,14 @@ class PostProcessPlugin
|
|
|
9437
9725
|
function postProcessRender()
|
|
9438
9726
|
{
|
|
9439
9727
|
if (headlessMode || !glEnable) return;
|
|
9440
|
-
|
|
9728
|
+
|
|
9441
9729
|
// clear out the buffer
|
|
9442
9730
|
glFlush();
|
|
9443
9731
|
|
|
9732
|
+
// ensure we render to the default framebuffer (in case any earlier
|
|
9733
|
+
// caller this frame left a render target bound)
|
|
9734
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
9735
|
+
|
|
9444
9736
|
// setup shader program to draw a quad
|
|
9445
9737
|
glContext.useProgram(postProcess.shader);
|
|
9446
9738
|
glContext.bindVertexArray(postProcess.vao);
|
|
@@ -9481,6 +9773,9 @@ class PostProcessPlugin
|
|
|
9481
9773
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
|
|
9482
9774
|
}
|
|
9483
9775
|
|
|
9776
|
+
// restore default so subsequent dynamic texture uploads aren't flipped
|
|
9777
|
+
glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
|
|
9778
|
+
|
|
9484
9779
|
// force it to set instanced mode
|
|
9485
9780
|
glSetInstancedMode(true);
|
|
9486
9781
|
}
|
|
@@ -10107,11 +10402,17 @@ class UISystemPlugin
|
|
|
10107
10402
|
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
10108
10403
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
10109
10404
|
{
|
|
10110
|
-
|
|
10405
|
+
// remove any prior listeners so repeated setup calls don't stack
|
|
10406
|
+
if (this._dragListeners)
|
|
10407
|
+
for (const [type, listener] of this._dragListeners)
|
|
10408
|
+
document.removeEventListener(type, listener);
|
|
10409
|
+
this._dragListeners = [];
|
|
10410
|
+
const setCallback = (callback, listenerType)=>
|
|
10111
10411
|
{
|
|
10112
|
-
|
|
10412
|
+
const listener = (e)=> { e.preventDefault(); callback && callback(e); };
|
|
10113
10413
|
document.addEventListener(listenerType, listener);
|
|
10114
|
-
|
|
10414
|
+
this._dragListeners.push([listenerType, listener]);
|
|
10415
|
+
};
|
|
10115
10416
|
setCallback(onDrop, 'drop');
|
|
10116
10417
|
setCallback(onDragEnter, 'dragenter');
|
|
10117
10418
|
setCallback(onDragLeave, 'dragleave');
|
|
@@ -10435,6 +10736,15 @@ class UIObject
|
|
|
10435
10736
|
if (this.destroyed)
|
|
10436
10737
|
return;
|
|
10437
10738
|
|
|
10739
|
+
// clear ui-system references that point at this object so events
|
|
10740
|
+
// don't keep firing against a destroyed target (especially the
|
|
10741
|
+
// keydown listener attached for keyInputObject)
|
|
10742
|
+
if (uiSystem.activeObject === this) uiSystem.activeObject = undefined;
|
|
10743
|
+
if (uiSystem.hoverObject === this) uiSystem.hoverObject = undefined;
|
|
10744
|
+
if (uiSystem.lastHoverObject === this) uiSystem.lastHoverObject = undefined;
|
|
10745
|
+
if (uiSystem.navigationObject === this) uiSystem.navigationObject = undefined;
|
|
10746
|
+
if (uiSystem.keyInputObject === this) uiSystem.keyInputObject = undefined;
|
|
10747
|
+
|
|
10438
10748
|
// disconnect from parent and destroy children
|
|
10439
10749
|
this.destroyed = 1;
|
|
10440
10750
|
this.parent?.removeChild(this);
|
|
@@ -10443,6 +10753,8 @@ class UIObject
|
|
|
10443
10753
|
child.parent = undefined;
|
|
10444
10754
|
child.destroy();
|
|
10445
10755
|
}
|
|
10756
|
+
// clear references so destroyed children can be GC'd
|
|
10757
|
+
this.children.length = 0;
|
|
10446
10758
|
}
|
|
10447
10759
|
|
|
10448
10760
|
/** Check if the mouse is overlapping this ui object
|
|
@@ -11032,6 +11344,7 @@ class UISlider extends UIObject
|
|
|
11032
11344
|
{
|
|
11033
11345
|
// toggle value between 0 and 1
|
|
11034
11346
|
this.value = this.value ? 0 : 1;
|
|
11347
|
+
this.onChange();
|
|
11035
11348
|
this.onRelease();
|
|
11036
11349
|
super.navigatePressed();
|
|
11037
11350
|
}
|
|
@@ -11391,6 +11704,11 @@ class Box2dObject extends EngineObject
|
|
|
11391
11704
|
// destroy physics body, fixtures, and joints
|
|
11392
11705
|
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
11393
11706
|
box2d.world.DestroyBody(this.body);
|
|
11707
|
+
|
|
11708
|
+
// remove from tracked list so paused / headless sessions don't leak
|
|
11709
|
+
const i = box2d.objects.indexOf(this);
|
|
11710
|
+
if (i >= 0)
|
|
11711
|
+
box2d.objects.splice(i, 1);
|
|
11394
11712
|
super.destroy();
|
|
11395
11713
|
}
|
|
11396
11714
|
|
|
@@ -11479,7 +11797,9 @@ class Box2dObject extends EngineObject
|
|
|
11479
11797
|
/** Add a box shape to the body
|
|
11480
11798
|
* @param {Vector2} [size]
|
|
11481
11799
|
* @param {Vector2} [offset]
|
|
11482
|
-
* @param {number} [angle]
|
|
11800
|
+
* @param {number} [angle] - LittleJS convention (clockwise positive).
|
|
11801
|
+
* Negated internally to match Box2D's CCW-positive convention so the
|
|
11802
|
+
* fixture aligns with the same angle passed to drawRect/drawTile.
|
|
11483
11803
|
* @param {number} [density]
|
|
11484
11804
|
* @param {number} [friction]
|
|
11485
11805
|
* @param {number} [restitution]
|
|
@@ -11492,7 +11812,7 @@ class Box2dObject extends EngineObject
|
|
|
11492
11812
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
11493
11813
|
|
|
11494
11814
|
const shape = new box2d.instance.b2PolygonShape();
|
|
11495
|
-
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
11815
|
+
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), -angle);
|
|
11496
11816
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
11497
11817
|
}
|
|
11498
11818
|
|
|
@@ -11794,9 +12114,10 @@ class Box2dObject extends EngineObject
|
|
|
11794
12114
|
{
|
|
11795
12115
|
const data = new box2d.instance.b2MassData();
|
|
11796
12116
|
this.body.GetMassData(data);
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
12117
|
+
// use !== undefined so setMass(0) (static-equivalent) isn't silently ignored
|
|
12118
|
+
if (localCenter !== undefined) data.set_center(box2d.vec2dTo(localCenter));
|
|
12119
|
+
if (mass !== undefined) data.set_mass(mass);
|
|
12120
|
+
if (momentOfInertia !== undefined) data.set_I(momentOfInertia);
|
|
11800
12121
|
this.body.SetMassData(data);
|
|
11801
12122
|
}
|
|
11802
12123
|
|
|
@@ -12968,6 +13289,8 @@ class Box2dPlugin
|
|
|
12968
13289
|
const fixtureB = contact.GetFixtureB();
|
|
12969
13290
|
const objectA = fixtureA.GetBody().object;
|
|
12970
13291
|
const objectB = fixtureB.GetBody().object;
|
|
13292
|
+
// raw user-created b2Bodies may have no .object — skip those
|
|
13293
|
+
if (!objectA || !objectB) return;
|
|
12971
13294
|
objectA.beginContact(objectB);
|
|
12972
13295
|
objectB.beginContact(objectA);
|
|
12973
13296
|
}
|
|
@@ -12978,6 +13301,7 @@ class Box2dPlugin
|
|
|
12978
13301
|
const fixtureB = contact.GetFixtureB();
|
|
12979
13302
|
const objectA = fixtureA.GetBody().object;
|
|
12980
13303
|
const objectB = fixtureB.GetBody().object;
|
|
13304
|
+
if (!objectA || !objectB) return;
|
|
12981
13305
|
objectA.endContact(objectB);
|
|
12982
13306
|
objectB.endContact(objectA);
|
|
12983
13307
|
};
|
|
@@ -13356,7 +13680,7 @@ async function box2dInit()
|
|
|
13356
13680
|
debugDraw.DrawTransform = function(transform)
|
|
13357
13681
|
{
|
|
13358
13682
|
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
|
|
13359
|
-
const pos =
|
|
13683
|
+
const pos = box2d.vec2From(transform.get_p());
|
|
13360
13684
|
const angle = -transform.get_q().GetAngle();
|
|
13361
13685
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
13362
13686
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
|
@@ -13565,13 +13889,21 @@ class Tween
|
|
|
13565
13889
|
}
|
|
13566
13890
|
ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
|
|
13567
13891
|
|
|
13892
|
+
/** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
|
|
13568
13893
|
this.callback = callback;
|
|
13894
|
+
/** @property {number|Vector2|Color} - Starting value */
|
|
13569
13895
|
this.start = start;
|
|
13896
|
+
/** @property {number|Vector2|Color} - Ending value */
|
|
13570
13897
|
this.end = end;
|
|
13898
|
+
/** @property {number} - Total duration in seconds */
|
|
13571
13899
|
this.duration = duration;
|
|
13900
|
+
/** @property {number} - Remaining time in seconds (counts down from duration to 0) */
|
|
13572
13901
|
this.life = duration;
|
|
13902
|
+
/** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
|
|
13573
13903
|
this.ease = options.ease || Ease.LINEAR;
|
|
13904
|
+
/** @property {boolean} - If true, advance even when the game is paused */
|
|
13574
13905
|
this.useRealTime = !!options.useRealTime;
|
|
13906
|
+
/** @property {boolean} - If true, stop advancing until cleared */
|
|
13575
13907
|
this.paused = !!options.paused;
|
|
13576
13908
|
|
|
13577
13909
|
/** @private completion callback set by then(), loop(), pingPong(). */
|
|
@@ -13752,7 +14084,7 @@ const Ease =
|
|
|
13752
14084
|
* @param {number} x
|
|
13753
14085
|
* @returns {number}
|
|
13754
14086
|
* @memberof TweenSystem */
|
|
13755
|
-
EXPO: (x) => 2 ** (10 * x - 10),
|
|
14087
|
+
EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
|
|
13756
14088
|
|
|
13757
14089
|
/** Back ease-in: overshoots backward at the start before snapping forward.
|
|
13758
14090
|
* @param {number} x
|
|
@@ -13765,6 +14097,8 @@ const Ease =
|
|
|
13765
14097
|
* @returns {number}
|
|
13766
14098
|
* @memberof TweenSystem */
|
|
13767
14099
|
ELASTIC: (x) =>
|
|
14100
|
+
x === 0 ? 0 :
|
|
14101
|
+
x === 1 ? 1 :
|
|
13768
14102
|
-(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
|
|
13769
14103
|
|
|
13770
14104
|
/** Spring-like ease-out: oscillates outward after passing the target.
|
|
@@ -13925,29 +14259,32 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
13925
14259
|
}
|
|
13926
14260
|
|
|
13927
14261
|
// Continuation that schedules the next loop iteration when one finishes.
|
|
13928
|
-
//
|
|
13929
|
-
//
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
14262
|
+
// Reuses the same Tween object across iterations so the user's handle
|
|
14263
|
+
// from `.loop()` keeps working — calling `.stop()` mid-loop now cancels
|
|
14264
|
+
// the entire chain instead of just the current iteration.
|
|
14265
|
+
function loopContinuation(tween)
|
|
14266
|
+
{
|
|
14267
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
14268
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
14269
|
+
tween.life = tween.duration;
|
|
14270
|
+
tween.thenCallback = () => loopContinuation(tween);
|
|
14271
|
+
tweenActive.push(tween);
|
|
14272
|
+
// snap to start for the new iteration (matches Tween constructor behavior)
|
|
14273
|
+
tween.callback(tween.interp(tween.duration));
|
|
14274
|
+
}
|
|
14275
|
+
|
|
14276
|
+
// Continuation for pingPong: swaps start and end on the same tween each iteration.
|
|
14277
|
+
function pingPongContinuation(tween)
|
|
14278
|
+
{
|
|
14279
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
14280
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
14281
|
+
const tmp = tween.start;
|
|
14282
|
+
tween.start = tween.end;
|
|
14283
|
+
tween.end = tmp;
|
|
14284
|
+
tween.life = tween.duration;
|
|
14285
|
+
tween.thenCallback = () => pingPongContinuation(tween);
|
|
14286
|
+
tweenActive.push(tween);
|
|
14287
|
+
tween.callback(tween.interp(tween.duration));
|
|
13951
14288
|
}
|
|
13952
14289
|
|
|
13953
14290
|
/** Engine plugin hook: advance every active tween by the appropriate delta.
|
|
@@ -14104,7 +14441,9 @@ class PathFinder
|
|
|
14104
14441
|
// .size + .getCollisionData.
|
|
14105
14442
|
if (isVector2(source))
|
|
14106
14443
|
{
|
|
14444
|
+
/** @property {Vector2} - Grid dimensions in tiles */
|
|
14107
14445
|
this.size = source.floor();
|
|
14446
|
+
/** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
|
|
14108
14447
|
this.tileLayer = undefined;
|
|
14109
14448
|
}
|
|
14110
14449
|
else
|
|
@@ -14116,13 +14455,18 @@ class PathFinder
|
|
|
14116
14455
|
}
|
|
14117
14456
|
|
|
14118
14457
|
// Tunables (public, freely re-assignable).
|
|
14458
|
+
/** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
|
|
14119
14459
|
this.heuristicWeight = 1;
|
|
14120
|
-
|
|
14460
|
+
/** @property {number} - Maximum A* expansions before giving up */
|
|
14461
|
+
this.maxLoop = 1e3;
|
|
14462
|
+
/** @property {boolean} - If true, post-process paths with two-pass smoothing */
|
|
14121
14463
|
this.smoothPath = true;
|
|
14464
|
+
/** @property {boolean} - If true, draw debug visualization during findPath */
|
|
14122
14465
|
this.debug = false;
|
|
14123
|
-
|
|
14466
|
+
/** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
|
|
14467
|
+
this.debugTime = 1;
|
|
14124
14468
|
|
|
14125
|
-
|
|
14469
|
+
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
|
|
14126
14470
|
this.nodes = new Array(this.size.x * this.size.y);
|
|
14127
14471
|
for (let y = 0; y < this.size.y; ++y)
|
|
14128
14472
|
for (let x = 0; x < this.size.x; ++x)
|
|
@@ -14274,11 +14618,13 @@ class PathFinder
|
|
|
14274
14618
|
if (dx !== 0 && dy !== 0)
|
|
14275
14619
|
{
|
|
14276
14620
|
// Diagonal step: refuse if either cardinal neighbor is
|
|
14277
|
-
// blocked
|
|
14621
|
+
// blocked. Prevents cutting through walls at corners.
|
|
14622
|
+
// (Costed-but-walkable cardinals do not block — diagonal
|
|
14623
|
+
// movement around expensive terrain is standard A*.)
|
|
14278
14624
|
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
14279
|
-
if (!card1 ||
|
|
14625
|
+
if (!card1 || !card1.walkable) continue;
|
|
14280
14626
|
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
14281
|
-
if (!card2 ||
|
|
14627
|
+
if (!card2 || !card2.walkable) continue;
|
|
14282
14628
|
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
14283
14629
|
}
|
|
14284
14630
|
|
|
@@ -14296,9 +14642,12 @@ class PathFinder
|
|
|
14296
14642
|
// Best path so far through neighbor — record it.
|
|
14297
14643
|
neighbor.parent = current;
|
|
14298
14644
|
neighbor.g = tentativeG;
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14645
|
+
// Octile heuristic — tightest admissible distance for an
|
|
14646
|
+
// 8-connected grid with cardinal cost 1 and diagonal cost √2.
|
|
14647
|
+
const adx = abs(endNode.pos.x - neighbor.pos.x);
|
|
14648
|
+
const ady = abs(endNode.pos.y - neighbor.pos.y);
|
|
14649
|
+
const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
|
|
14650
|
+
neighbor.f = neighbor.g + h * this.heuristicWeight;
|
|
14302
14651
|
}
|
|
14303
14652
|
}
|
|
14304
14653
|
|
|
@@ -14580,6 +14929,24 @@ class PathFinder
|
|
|
14580
14929
|
path.push(original[original.length - 1]);
|
|
14581
14930
|
}
|
|
14582
14931
|
|
|
14932
|
+
/** Drop any middle node that lies exactly on the line through its two
|
|
14933
|
+
* neighbors. Backstop for the smoothing passes — the corners pass
|
|
14934
|
+
* intentionally keeps truly-straight runs, and the string-pulling pass
|
|
14935
|
+
* checks collinearity against the original path, not the in-progress
|
|
14936
|
+
* result, so it can leave 3+ collinear nodes in some edge cases.
|
|
14937
|
+
* @param {PathFinderNode[]} path
|
|
14938
|
+
* @private */
|
|
14939
|
+
dropCollinearNodes(path)
|
|
14940
|
+
{
|
|
14941
|
+
for (let i = path.length - 2; i >= 1; --i)
|
|
14942
|
+
{
|
|
14943
|
+
const a = path[i - 1], b = path[i], c = path[i + 1];
|
|
14944
|
+
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
|
|
14945
|
+
(b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
|
|
14946
|
+
path.splice(i, 1);
|
|
14947
|
+
}
|
|
14948
|
+
}
|
|
14949
|
+
|
|
14583
14950
|
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
14584
14951
|
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
14585
14952
|
* @param {number} x
|
|
@@ -14747,6 +15114,7 @@ class PathFinder
|
|
|
14747
15114
|
{
|
|
14748
15115
|
this.smoothPathCorners(nodePath);
|
|
14749
15116
|
this.smoothPathStringPull(nodePath);
|
|
15117
|
+
this.dropCollinearNodes(nodePath);
|
|
14750
15118
|
}
|
|
14751
15119
|
|
|
14752
15120
|
// Convert to world-space Vector2 path. Return copies, not live node
|