littlejsengine 1.18.7 → 1.18.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/dist/littlejs.d.ts +76 -8
- package/dist/littlejs.esm.js +333 -73
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +327 -72
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +317 -64
- package/package.json +1 -1
- package/plugins/medalSystem.js +2 -1
- package/plugins/newgrounds.js +7 -0
- package/plugins/pathFinder.js +35 -6
- package/plugins/tweenSystem.js +9 -1
- package/plugins/uiSystem.js +36 -14
- package/src/engine.js +3 -2
- package/src/engineAudio.js +7 -9
- package/src/engineDebug.js +10 -8
- package/src/engineDraw.js +94 -14
- package/src/engineExport.js +6 -1
- package/src/engineInput.js +7 -7
- package/src/engineMath.js +5 -3
- package/src/engineUtilities.js +49 -6
- package/src/engineWebGL.js +63 -1
- package/plugins/tween.js +0 -509
package/dist/littlejs.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
38
|
+
const engineVersion = '1.18.12';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -207,7 +207,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
207
207
|
const combinedScale = timeScale * debugScale;
|
|
208
208
|
frameTimeDeltaMS *= combinedScale;
|
|
209
209
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
210
|
-
if (
|
|
210
|
+
if (combinedScale <= 1)
|
|
211
211
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
212
212
|
|
|
213
213
|
let wasUpdated = false;
|
|
@@ -294,6 +294,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
294
294
|
glFlush();
|
|
295
295
|
debugRenderPost();
|
|
296
296
|
drawCount = 0;
|
|
297
|
+
primitiveCount = 0;
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -645,7 +646,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
|
|
|
645
646
|
{
|
|
646
647
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
647
648
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
648
|
-
ASSERT(
|
|
649
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
649
650
|
ASSERT(isNumber(time), 'time must be a number');
|
|
650
651
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
651
652
|
|
|
@@ -672,7 +673,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screen
|
|
|
672
673
|
{
|
|
673
674
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
674
675
|
ASSERT(isArray(points), 'points must be an array');
|
|
675
|
-
ASSERT(
|
|
676
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
676
677
|
ASSERT(isNumber(time), 'time must be a number');
|
|
677
678
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
678
679
|
|
|
@@ -696,7 +697,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=f
|
|
|
696
697
|
{
|
|
697
698
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
698
699
|
ASSERT(isNumber(size), 'size must be a number');
|
|
699
|
-
ASSERT(
|
|
700
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
700
701
|
ASSERT(isNumber(time), 'time must be a number');
|
|
701
702
|
|
|
702
703
|
if (isColor(color))
|
|
@@ -774,13 +775,13 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
774
775
|
* @memberof Debug */
|
|
775
776
|
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
|
|
776
777
|
{
|
|
777
|
-
ASSERT(
|
|
778
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
778
779
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
779
780
|
ASSERT(isNumber(size), 'size must be a number');
|
|
780
|
-
ASSERT(
|
|
781
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
781
782
|
ASSERT(isNumber(time), 'time must be a number');
|
|
782
783
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
783
|
-
ASSERT(
|
|
784
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
784
785
|
|
|
785
786
|
if (isColor(color))
|
|
786
787
|
color = color.toString();
|
|
@@ -1068,7 +1069,8 @@ function debugRender()
|
|
|
1068
1069
|
debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
|
|
1069
1070
|
x, y += h);
|
|
1070
1071
|
debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
|
|
1071
|
-
debugContext.fillText('Draw
|
|
1072
|
+
debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
|
|
1073
|
+
debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
|
|
1072
1074
|
debugContext.fillText('---------', x, y += h);
|
|
1073
1075
|
debugContext.fillStyle = '#f00';
|
|
1074
1076
|
debugContext.fillText('ESC: Debug Overlay', x, y += h);
|
|
@@ -1139,7 +1141,8 @@ function debugRenderPost()
|
|
|
1139
1141
|
mainContext.font = '1em monospace';
|
|
1140
1142
|
mainContext.fillStyle = '#000';
|
|
1141
1143
|
const text = engineName + ' v' + engineVersion + ' / '
|
|
1142
|
-
+ drawCount + ' / ' +
|
|
1144
|
+
+ drawCount + ' / ' + primitiveCount + ' / '
|
|
1145
|
+
+ engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
1143
1146
|
+ (glEnable ? ' GL' : ' 2D') ;
|
|
1144
1147
|
mainContext.fillText(text, mainCanvas.width-3, 3);
|
|
1145
1148
|
mainContext.fillStyle = '#fff';
|
|
@@ -1580,13 +1583,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
|
|
|
1580
1583
|
function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
1581
1584
|
|
|
1582
1585
|
/**
|
|
1583
|
-
* Check if
|
|
1586
|
+
* Check if a value is stringifiable — i.e. it has a toString that returns
|
|
1587
|
+
* a string. Use this for ASSERTs and inputs that will be coerced to text;
|
|
1588
|
+
* use `typeof x === 'string'` inline if you need strict-string semantics.
|
|
1584
1589
|
* - Returns true for strings, numbers, and most objects
|
|
1585
1590
|
* - Returns false for null and undefined
|
|
1586
1591
|
* @param {any} s
|
|
1587
1592
|
* @return {boolean}
|
|
1588
1593
|
* @memberof Math */
|
|
1589
|
-
function
|
|
1594
|
+
function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
|
|
1590
1595
|
|
|
1591
1596
|
/**
|
|
1592
1597
|
* Check if object is an array
|
|
@@ -2330,7 +2335,7 @@ class Color
|
|
|
2330
2335
|
* @return {Color} */
|
|
2331
2336
|
setHex(hex)
|
|
2332
2337
|
{
|
|
2333
|
-
ASSERT(
|
|
2338
|
+
ASSERT(isStringLike(hex), 'Color hex code must be a string');
|
|
2334
2339
|
ASSERT(hex[0] === '#', 'Color hex code must start with #');
|
|
2335
2340
|
ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
|
|
2336
2341
|
|
|
@@ -2447,6 +2452,7 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
2447
2452
|
* - File saving (text, canvas, data URLs)
|
|
2448
2453
|
* - Native share dialog support
|
|
2449
2454
|
* - Local storage save data management
|
|
2455
|
+
* - Gradient noise (1D and 2D)
|
|
2450
2456
|
* @namespace Utilities
|
|
2451
2457
|
*/
|
|
2452
2458
|
|
|
@@ -2594,8 +2600,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
2594
2600
|
* @memberof Utilities */
|
|
2595
2601
|
function saveDataURL(url, filename='download', revokeTime)
|
|
2596
2602
|
{
|
|
2597
|
-
ASSERT(
|
|
2598
|
-
ASSERT(
|
|
2603
|
+
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
2604
|
+
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
2599
2605
|
|
|
2600
2606
|
// create link for saving screenshots
|
|
2601
2607
|
const link = document.createElement('a');
|
|
@@ -2613,8 +2619,8 @@ function saveDataURL(url, filename='download', revokeTime)
|
|
|
2613
2619
|
* @memberof Utilities */
|
|
2614
2620
|
function shareURL(title, url, callback)
|
|
2615
2621
|
{
|
|
2616
|
-
ASSERT(
|
|
2617
|
-
ASSERT(
|
|
2622
|
+
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
2623
|
+
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
2618
2624
|
navigator.share?.({title, url}).then(()=>callback?.());
|
|
2619
2625
|
}
|
|
2620
2626
|
|
|
@@ -2627,7 +2633,7 @@ function shareURL(title, url, callback)
|
|
|
2627
2633
|
* @memberof Utilities */
|
|
2628
2634
|
function readSaveData(saveName, defaultSaveData)
|
|
2629
2635
|
{
|
|
2630
|
-
ASSERT(
|
|
2636
|
+
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
2631
2637
|
|
|
2632
2638
|
// replace undefined values with defaults; tolerate corrupt JSON
|
|
2633
2639
|
const data = localStorage[saveName];
|
|
@@ -2646,8 +2652,50 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
2646
2652
|
* @memberof Utilities */
|
|
2647
2653
|
function writeSaveData(saveName, saveData)
|
|
2648
2654
|
{
|
|
2649
|
-
ASSERT(
|
|
2655
|
+
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
2650
2656
|
localStorage[saveName] = JSON.stringify(saveData);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2660
|
+
|
|
2661
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
2662
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
2663
|
+
function noiseHash(i)
|
|
2664
|
+
{
|
|
2665
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
2666
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
2667
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
2668
|
+
h ^= h >>> 16;
|
|
2669
|
+
return (h >>> 0) / 2**32;
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
/** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
|
|
2673
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
2674
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
2675
|
+
* @param {number} x
|
|
2676
|
+
* @return {number}
|
|
2677
|
+
* @memberof Utilities */
|
|
2678
|
+
function noise1D(x)
|
|
2679
|
+
{
|
|
2680
|
+
const i = floor(x);
|
|
2681
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
2685
|
+
* @param {number} x
|
|
2686
|
+
* @param {number} y
|
|
2687
|
+
* @return {number}
|
|
2688
|
+
* @memberof Utilities */
|
|
2689
|
+
function noise2D(x, y)
|
|
2690
|
+
{
|
|
2691
|
+
const ix = floor(x), iy = floor(y);
|
|
2692
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
2693
|
+
// large prime decorrelates neighboring rows
|
|
2694
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
2695
|
+
return lerp(
|
|
2696
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
2697
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
2698
|
+
fy);
|
|
2651
2699
|
}
|
|
2652
2700
|
/**
|
|
2653
2701
|
* LittleJS Engine Settings
|
|
@@ -3854,6 +3902,12 @@ let textureInfos = [];
|
|
|
3854
3902
|
* @memberof Draw */
|
|
3855
3903
|
let drawCount;
|
|
3856
3904
|
|
|
3905
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
3906
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
3907
|
+
* @type {number}
|
|
3908
|
+
* @memberof Draw */
|
|
3909
|
+
let primitiveCount;
|
|
3910
|
+
|
|
3857
3911
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
3858
3912
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
3859
3913
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
@@ -4094,19 +4148,19 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
4094
4148
|
}
|
|
4095
4149
|
else
|
|
4096
4150
|
{
|
|
4097
|
-
//
|
|
4098
|
-
//
|
|
4099
|
-
//
|
|
4100
|
-
// color.add(additiveColor) on line ~337.
|
|
4151
|
+
// untextured: glDrawUntextured picks the optimal path (poly
|
|
4152
|
+
// tristrip if already in poly mode, otherwise instanced with
|
|
4153
|
+
// uvs/rgba zeroed). Color+additive are folded together to match
|
|
4154
|
+
// the Canvas2D path's color.add(additiveColor) on line ~337.
|
|
4101
4155
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
4102
|
-
|
|
4103
|
-
0, combined.rgbaInt());
|
|
4156
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
4104
4157
|
}
|
|
4105
4158
|
}
|
|
4106
4159
|
else
|
|
4107
4160
|
{
|
|
4108
4161
|
// normal canvas 2D rendering method (slower)
|
|
4109
4162
|
++drawCount;
|
|
4163
|
+
++primitiveCount;
|
|
4110
4164
|
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
4111
4165
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
4112
4166
|
{
|
|
@@ -4146,13 +4200,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
4146
4200
|
* @param {Vector2} pos
|
|
4147
4201
|
* @param {Vector2} [size=vec2(1)]
|
|
4148
4202
|
* @param {Color} [colorTop=WHITE]
|
|
4149
|
-
* @param {Color} [colorBottom=
|
|
4203
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
4150
4204
|
* @param {number} [angle]
|
|
4151
4205
|
* @param {boolean} [useWebGL=glEnable]
|
|
4152
4206
|
* @param {boolean} [screenSpace]
|
|
4153
4207
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4154
4208
|
* @memberof Draw */
|
|
4155
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=
|
|
4209
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
4156
4210
|
{
|
|
4157
4211
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4158
4212
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -4192,6 +4246,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4192
4246
|
{
|
|
4193
4247
|
// normal canvas 2D rendering method (slower)
|
|
4194
4248
|
++drawCount;
|
|
4249
|
+
++primitiveCount;
|
|
4195
4250
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
4196
4251
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
4197
4252
|
{
|
|
@@ -4254,8 +4309,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
4254
4309
|
return;
|
|
4255
4310
|
}
|
|
4256
4311
|
|
|
4257
|
-
// Canvas2D path — increment
|
|
4312
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
4258
4313
|
++drawCount;
|
|
4314
|
+
++primitiveCount;
|
|
4259
4315
|
|
|
4260
4316
|
if (!screenSpace)
|
|
4261
4317
|
{
|
|
@@ -4329,6 +4385,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
4329
4385
|
{
|
|
4330
4386
|
// normal canvas 2D rendering method (slower)
|
|
4331
4387
|
++drawCount;
|
|
4388
|
+
++primitiveCount;
|
|
4332
4389
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
4333
4390
|
{
|
|
4334
4391
|
context.strokeStyle = color.toString();
|
|
@@ -4509,6 +4566,77 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
4509
4566
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
4510
4567
|
}
|
|
4511
4568
|
|
|
4569
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
4570
|
+
* - Best when batched with other untextured polys
|
|
4571
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
4572
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
4573
|
+
* @param {Vector2} pos
|
|
4574
|
+
* @param {number} [size=1] - Diameter
|
|
4575
|
+
* @param {Color} [colorInner=WHITE]
|
|
4576
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
4577
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
4578
|
+
* @param {boolean} [screenSpace]
|
|
4579
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
4580
|
+
* @memberof Draw */
|
|
4581
|
+
let drawCircleGradientOffset = 0;
|
|
4582
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
4583
|
+
{
|
|
4584
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4585
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
4586
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
4587
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
4588
|
+
|
|
4589
|
+
if (headlessMode) return;
|
|
4590
|
+
|
|
4591
|
+
if (useWebGL && glEnable)
|
|
4592
|
+
{
|
|
4593
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
4594
|
+
if (screenSpace)
|
|
4595
|
+
{
|
|
4596
|
+
// convert to world space
|
|
4597
|
+
pos = screenToWorld(pos);
|
|
4598
|
+
size /= cameraScale;
|
|
4599
|
+
}
|
|
4600
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
4601
|
+
// so back-to-back gradients at the same position have their hole
|
|
4602
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
4603
|
+
// verts and don't visibly stack
|
|
4604
|
+
const sides = glCircleSides;
|
|
4605
|
+
const radius = size/2;
|
|
4606
|
+
const innerInt = colorInner.rgbaInt();
|
|
4607
|
+
const outerInt = colorOuter.rgbaInt();
|
|
4608
|
+
const offset = drawCircleGradientOffset++;
|
|
4609
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
4610
|
+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
|
|
4611
|
+
const colors = [outerInt];
|
|
4612
|
+
for (let i=sides; i--;)
|
|
4613
|
+
{
|
|
4614
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
4615
|
+
points.push(pos);
|
|
4616
|
+
colors.push(innerInt);
|
|
4617
|
+
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
|
|
4618
|
+
colors.push(outerInt);
|
|
4619
|
+
}
|
|
4620
|
+
glDrawColoredPoints(points, colors);
|
|
4621
|
+
}
|
|
4622
|
+
else
|
|
4623
|
+
{
|
|
4624
|
+
// normal canvas 2D rendering method (slower)
|
|
4625
|
+
++drawCount;
|
|
4626
|
+
++primitiveCount;
|
|
4627
|
+
drawCanvas2D(pos, vec2(size), 0, false, (context)=>
|
|
4628
|
+
{
|
|
4629
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
4630
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
4631
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
4632
|
+
context.fillStyle = gradient;
|
|
4633
|
+
context.beginPath();
|
|
4634
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
4635
|
+
context.fill();
|
|
4636
|
+
}, screenSpace, context);
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4512
4640
|
/**
|
|
4513
4641
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
4514
4642
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -4592,15 +4720,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
4592
4720
|
* @memberof Draw */
|
|
4593
4721
|
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
4594
4722
|
{
|
|
4595
|
-
ASSERT(
|
|
4723
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
4596
4724
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4597
4725
|
ASSERT(isNumber(size), 'size must be a number');
|
|
4598
4726
|
ASSERT(isColor(color), 'color must be a color');
|
|
4599
4727
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
4600
4728
|
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
4601
4729
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
4602
|
-
ASSERT(
|
|
4603
|
-
ASSERT(
|
|
4730
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
4731
|
+
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
4604
4732
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4605
4733
|
|
|
4606
4734
|
context.fillStyle = color.toString();
|
|
@@ -4637,7 +4765,7 @@ async function loadTexture(textureIndex, src)
|
|
|
4637
4765
|
{
|
|
4638
4766
|
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
4639
4767
|
ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
|
|
4640
|
-
ASSERT(!src ||
|
|
4768
|
+
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
4641
4769
|
|
|
4642
4770
|
const image = new Image;
|
|
4643
4771
|
if (src)
|
|
@@ -5025,7 +5153,7 @@ class FontImage
|
|
|
5025
5153
|
*/
|
|
5026
5154
|
drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
|
|
5027
5155
|
{
|
|
5028
|
-
ASSERT(
|
|
5156
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
5029
5157
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
5030
5158
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
5031
5159
|
ASSERT(isColor(color), 'color must be a color');
|
|
@@ -5186,7 +5314,7 @@ function inputClear()
|
|
|
5186
5314
|
* @memberof Input */
|
|
5187
5315
|
function keyIsDown(key, device=0)
|
|
5188
5316
|
{
|
|
5189
|
-
ASSERT(
|
|
5317
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
5190
5318
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
5191
5319
|
return !!(inputData[device]?.[key] & 1);
|
|
5192
5320
|
}
|
|
@@ -5198,7 +5326,7 @@ function keyIsDown(key, device=0)
|
|
|
5198
5326
|
* @memberof Input */
|
|
5199
5327
|
function keyWasPressed(key, device=0)
|
|
5200
5328
|
{
|
|
5201
|
-
ASSERT(
|
|
5329
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
5202
5330
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
5203
5331
|
return !!(inputData[device]?.[key] & 2);
|
|
5204
5332
|
}
|
|
@@ -5210,7 +5338,7 @@ function keyWasPressed(key, device=0)
|
|
|
5210
5338
|
* @memberof Input */
|
|
5211
5339
|
function keyWasReleased(key, device=0)
|
|
5212
5340
|
{
|
|
5213
|
-
ASSERT(
|
|
5341
|
+
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
5214
5342
|
ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
|
|
5215
5343
|
return !!(inputData[device]?.[key] & 4);
|
|
5216
5344
|
}
|
|
@@ -5224,10 +5352,10 @@ function keyWasReleased(key, device=0)
|
|
|
5224
5352
|
* @memberof Input */
|
|
5225
5353
|
function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
|
|
5226
5354
|
{
|
|
5227
|
-
ASSERT(
|
|
5228
|
-
ASSERT(
|
|
5229
|
-
ASSERT(
|
|
5230
|
-
ASSERT(
|
|
5355
|
+
ASSERT(isStringLike(up), 'up key must be a string');
|
|
5356
|
+
ASSERT(isStringLike(down), 'down key must be a string');
|
|
5357
|
+
ASSERT(isStringLike(left), 'left key must be a string');
|
|
5358
|
+
ASSERT(isStringLike(right), 'right key must be a string');
|
|
5231
5359
|
const k = (key)=> keyIsDown(key) ? 1 : 0;
|
|
5232
5360
|
return vec2(k(right) - k(left), k(up) - k(down));
|
|
5233
5361
|
}
|
|
@@ -5945,12 +6073,10 @@ function touchGamepadButtonCenter()
|
|
|
5945
6073
|
* @namespace Audio
|
|
5946
6074
|
*/
|
|
5947
6075
|
|
|
5948
|
-
/** Audio context used by the engine
|
|
5949
|
-
* browser autoplay warnings about constructing an AudioContext before any
|
|
5950
|
-
* user gesture.
|
|
6076
|
+
/** Audio context used by the engine
|
|
5951
6077
|
* @type {AudioContext}
|
|
5952
6078
|
* @memberof Audio */
|
|
5953
|
-
let audioContext;
|
|
6079
|
+
let audioContext = new AudioContext;
|
|
5954
6080
|
|
|
5955
6081
|
/** Master gain node for all audio to pass through
|
|
5956
6082
|
* @type {GainNode}
|
|
@@ -5966,13 +6092,12 @@ const audioDefaultSampleRate = 44100;
|
|
|
5966
6092
|
* @return {boolean} - True if the audio context is running
|
|
5967
6093
|
* @memberof Audio */
|
|
5968
6094
|
function audioIsRunning()
|
|
5969
|
-
{ return audioContext
|
|
6095
|
+
{ return audioContext.state === 'running'; }
|
|
5970
6096
|
|
|
5971
6097
|
function audioInit()
|
|
5972
6098
|
{
|
|
5973
6099
|
if (!soundEnable || headlessMode) return;
|
|
5974
6100
|
|
|
5975
|
-
audioContext = new AudioContext;
|
|
5976
6101
|
audioMasterGain = audioContext.createGain();
|
|
5977
6102
|
audioMasterGain.connect(audioContext.destination);
|
|
5978
6103
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -6018,7 +6143,7 @@ class Sound
|
|
|
6018
6143
|
{
|
|
6019
6144
|
if (!soundEnable || headlessMode) return;
|
|
6020
6145
|
|
|
6021
|
-
ASSERT(!asset || isArray(asset) ||
|
|
6146
|
+
ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
|
|
6022
6147
|
ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
|
|
6023
6148
|
ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
|
|
6024
6149
|
ASSERT(isNumber(range), 'range must be a number');
|
|
@@ -6357,14 +6482,15 @@ class SoundInstance
|
|
|
6357
6482
|
|
|
6358
6483
|
/** Speak text with passed in settings
|
|
6359
6484
|
* @param {string} text - The text to speak
|
|
6360
|
-
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
6361
6485
|
* @param {number} [volume] - How much to scale volume by
|
|
6362
6486
|
* @param {number} [rate] - How quickly to speak
|
|
6363
6487
|
* @param {number} [pitch] - How much to change the pitch by
|
|
6488
|
+
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
6364
6489
|
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
6365
6490
|
* @memberof Audio */
|
|
6366
|
-
function speak(text,
|
|
6491
|
+
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
6367
6492
|
{
|
|
6493
|
+
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
6368
6494
|
if (!soundEnable || headlessMode) return;
|
|
6369
6495
|
if (!speechSynthesis) return;
|
|
6370
6496
|
|
|
@@ -8357,7 +8483,8 @@ function glFlush()
|
|
|
8357
8483
|
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
8358
8484
|
else
|
|
8359
8485
|
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
8360
|
-
drawCount
|
|
8486
|
+
++drawCount;
|
|
8487
|
+
primitiveCount += glBatchCount;
|
|
8361
8488
|
glBatchCount = 0;
|
|
8362
8489
|
}
|
|
8363
8490
|
glBatchAdditive = glAdditive;
|
|
@@ -8418,6 +8545,67 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
8418
8545
|
glPositionData[offset++] = angle;
|
|
8419
8546
|
}
|
|
8420
8547
|
|
|
8548
|
+
/** Add an untextured rect to the gl draw list
|
|
8549
|
+
* Picks the optimal path: if already in poly mode, emits a tristrip rect
|
|
8550
|
+
* so it batches with surrounding polys; otherwise uses the instanced path
|
|
8551
|
+
* with uvs and rgba zeroed so the color falls through the additive slot.
|
|
8552
|
+
* @param {number} x
|
|
8553
|
+
* @param {number} y
|
|
8554
|
+
* @param {number} sizeX
|
|
8555
|
+
* @param {number} sizeY
|
|
8556
|
+
* @param {number} angle
|
|
8557
|
+
* @param {number} rgba - color as 32-bit integer
|
|
8558
|
+
* @memberof WebGL */
|
|
8559
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
8560
|
+
{
|
|
8561
|
+
if (glPolyMode)
|
|
8562
|
+
{
|
|
8563
|
+
// batch with surrounding polys as a 4-vertex tristrip rect
|
|
8564
|
+
const vertCount = 6; // 4 corners + 2 degenerate verts
|
|
8565
|
+
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
8566
|
+
glFlush();
|
|
8567
|
+
|
|
8568
|
+
// compute rotated corners in world space (matches glDrawPointsTransform rotation)
|
|
8569
|
+
const hx = sizeX*.5, hy = sizeY*.5;
|
|
8570
|
+
const c = cos(angle), s = sin(angle);
|
|
8571
|
+
const chx = c*hx, shx = s*hx, chy = c*hy, shy = s*hy;
|
|
8572
|
+
const x0 = x - chx - shy, y0 = y + shx - chy; // (-hx,-hy)
|
|
8573
|
+
const x1 = x + chx - shy, y1 = y - shx - chy; // ( hx,-hy)
|
|
8574
|
+
const x2 = x - chx + shy, y2 = y + shx + chy; // (-hx, hy)
|
|
8575
|
+
const x3 = x + chx + shy, y3 = y - shx + chy; // ( hx, hy)
|
|
8576
|
+
|
|
8577
|
+
// write tristrip with leading/trailing degenerate verts
|
|
8578
|
+
let offset = glBatchCount * gl_INDICES_PER_POLY_VERTEX;
|
|
8579
|
+
glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
|
|
8580
|
+
glPositionData[offset++] = x0; glPositionData[offset++] = y0; glColorData[offset++] = rgba;
|
|
8581
|
+
glPositionData[offset++] = x1; glPositionData[offset++] = y1; glColorData[offset++] = rgba;
|
|
8582
|
+
glPositionData[offset++] = x2; glPositionData[offset++] = y2; glColorData[offset++] = rgba;
|
|
8583
|
+
glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
|
|
8584
|
+
glPositionData[offset++] = x3; glPositionData[offset++] = y3; glColorData[offset++] = rgba;
|
|
8585
|
+
glBatchCount += vertCount;
|
|
8586
|
+
return;
|
|
8587
|
+
}
|
|
8588
|
+
|
|
8589
|
+
// instanced path: zero uvs and rgba so the texture contribution is killed,
|
|
8590
|
+
// then carry the real color in the additive slot
|
|
8591
|
+
if (glBatchCount >= gl_MAX_INSTANCES || glBatchAdditive !== glAdditive)
|
|
8592
|
+
glFlush();
|
|
8593
|
+
glSetInstancedMode();
|
|
8594
|
+
|
|
8595
|
+
let offset = glBatchCount++ * gl_INDICES_PER_INSTANCE;
|
|
8596
|
+
glPositionData[offset++] = x;
|
|
8597
|
+
glPositionData[offset++] = y;
|
|
8598
|
+
glPositionData[offset++] = sizeX;
|
|
8599
|
+
glPositionData[offset++] = sizeY;
|
|
8600
|
+
glPositionData[offset++] = 0;
|
|
8601
|
+
glPositionData[offset++] = 0;
|
|
8602
|
+
glPositionData[offset++] = 0;
|
|
8603
|
+
glPositionData[offset++] = 0;
|
|
8604
|
+
glColorData[offset++] = 0;
|
|
8605
|
+
glColorData[offset++] = rgba;
|
|
8606
|
+
glPositionData[offset++] = angle;
|
|
8607
|
+
}
|
|
8608
|
+
|
|
8421
8609
|
/** Transform and add a polygon to the gl draw list
|
|
8422
8610
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
8423
8611
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -9054,7 +9242,8 @@ class Medal
|
|
|
9054
9242
|
/** @property {boolean} - Is the medal unlocked? */
|
|
9055
9243
|
this.unlocked = false;
|
|
9056
9244
|
|
|
9057
|
-
|
|
9245
|
+
/** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
|
|
9246
|
+
this.image = undefined;
|
|
9058
9247
|
if (src)
|
|
9059
9248
|
(this.image = new Image).src = src;
|
|
9060
9249
|
|
|
@@ -9217,13 +9406,18 @@ class NewgroundsPlugin
|
|
|
9217
9406
|
ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
|
|
9218
9407
|
|
|
9219
9408
|
newgrounds = this; // set global newgrounds object
|
|
9409
|
+
/** @property {string} - The newgrounds App ID */
|
|
9220
9410
|
this.app_id = app_id;
|
|
9411
|
+
/** @property {string|undefined} - AES-128/Base64 encryption key, if any */
|
|
9221
9412
|
this.cipher = cipher;
|
|
9413
|
+
/** @property {Object|undefined} - CryptoJS instance used when cipher is set */
|
|
9222
9414
|
this.cryptoJS = cryptoJS;
|
|
9415
|
+
/** @property {string} - Hostname used when logging views */
|
|
9223
9416
|
this.host = location ? location.hostname : '';
|
|
9224
9417
|
|
|
9225
9418
|
// get session id from url search params
|
|
9226
9419
|
const url = new URL(location.href);
|
|
9420
|
+
/** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
|
|
9227
9421
|
this.session_id = url.searchParams.get('ngio_session_id');
|
|
9228
9422
|
|
|
9229
9423
|
if (!this.session_id)
|
|
@@ -9231,6 +9425,7 @@ class NewgroundsPlugin
|
|
|
9231
9425
|
|
|
9232
9426
|
// get medals
|
|
9233
9427
|
const medalsResult = this.call('Medal.getList');
|
|
9428
|
+
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
9234
9429
|
this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
|
|
9235
9430
|
debugMedals && LOG(this.medals);
|
|
9236
9431
|
for (const newgroundsMedal of this.medals)
|
|
@@ -9254,6 +9449,7 @@ class NewgroundsPlugin
|
|
|
9254
9449
|
|
|
9255
9450
|
// get scoreboards
|
|
9256
9451
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
9452
|
+
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
9257
9453
|
this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
|
|
9258
9454
|
debugMedals && LOG(this.scoreboards);
|
|
9259
9455
|
|
|
@@ -9777,12 +9973,31 @@ class UISystemPlugin
|
|
|
9777
9973
|
|
|
9778
9974
|
engineAddPlugin(uiUpdate, uiRender);
|
|
9779
9975
|
|
|
9780
|
-
// set object position
|
|
9976
|
+
// set object position based on anchor target (parent box, or canvas for roots),
|
|
9977
|
+
// self-pivot, and localPos offset
|
|
9781
9978
|
function updateTransforms(o)
|
|
9782
9979
|
{
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9980
|
+
let targetPos, targetSize;
|
|
9981
|
+
if (o.parent)
|
|
9982
|
+
{
|
|
9983
|
+
targetPos = o.parent.pos;
|
|
9984
|
+
targetSize = o.parent.size;
|
|
9985
|
+
}
|
|
9986
|
+
else
|
|
9987
|
+
{
|
|
9988
|
+
// anchor to canvas in native coords (handles nativeHeight if set)
|
|
9989
|
+
targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
9990
|
+
targetSize = uiSystem.nativeHeight
|
|
9991
|
+
? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
|
|
9992
|
+
uiSystem.nativeHeight)
|
|
9993
|
+
: mainCanvasSize;
|
|
9994
|
+
}
|
|
9995
|
+
|
|
9996
|
+
const a = o.anchor;
|
|
9997
|
+
o.pos = targetPos
|
|
9998
|
+
.add(targetSize.multiply(a).scale(.5)) // anchor point on target
|
|
9999
|
+
.subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
|
|
10000
|
+
.add(o.localPos); // user offset
|
|
9786
10001
|
}
|
|
9787
10002
|
|
|
9788
10003
|
// setup recursive update and render
|
|
@@ -10249,9 +10464,8 @@ class UISystemPlugin
|
|
|
10249
10464
|
// confirm menu
|
|
10250
10465
|
const confirmMenu = new UIObject(vec2(), size);
|
|
10251
10466
|
uiSystem.confirmDialog = confirmMenu;
|
|
10252
|
-
confirmMenu.onRender = ()=>
|
|
10467
|
+
confirmMenu.onRender = ()=>
|
|
10253
10468
|
{
|
|
10254
|
-
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
10255
10469
|
const backgroundColor = hsl(0,0,0,.7);
|
|
10256
10470
|
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
10257
10471
|
}
|
|
@@ -10384,7 +10598,11 @@ class UIObject
|
|
|
10384
10598
|
this.navigationIndex = undefined;
|
|
10385
10599
|
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
10386
10600
|
this.navigationAutoSelect = false;
|
|
10387
|
-
|
|
10601
|
+
/** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
|
|
10602
|
+
* Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
|
|
10603
|
+
* Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
|
|
10604
|
+
this.anchor = vec2();
|
|
10605
|
+
|
|
10388
10606
|
uiSystem.uiObjects.push(this);
|
|
10389
10607
|
}
|
|
10390
10608
|
|
|
@@ -10637,9 +10855,9 @@ class UIText extends UIObject
|
|
|
10637
10855
|
{
|
|
10638
10856
|
super(pos, size);
|
|
10639
10857
|
|
|
10640
|
-
ASSERT(
|
|
10858
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
10641
10859
|
ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
|
|
10642
|
-
ASSERT(
|
|
10860
|
+
ASSERT(isStringLike(font), 'ui text font must be a string');
|
|
10643
10861
|
|
|
10644
10862
|
// set properties
|
|
10645
10863
|
this.text = text;
|
|
@@ -10687,7 +10905,7 @@ class UITextInput extends UIObject
|
|
|
10687
10905
|
{
|
|
10688
10906
|
super(pos, size);
|
|
10689
10907
|
|
|
10690
|
-
ASSERT(
|
|
10908
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
10691
10909
|
|
|
10692
10910
|
/** @property {number} - Max length of input (0 = no limit) */
|
|
10693
10911
|
this.maxLength = 0;
|
|
@@ -10824,7 +11042,7 @@ class UIButton extends UIObject
|
|
|
10824
11042
|
{
|
|
10825
11043
|
super(pos, size);
|
|
10826
11044
|
|
|
10827
|
-
ASSERT(
|
|
11045
|
+
ASSERT(isStringLike(text), 'ui button must be a string');
|
|
10828
11046
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
10829
11047
|
|
|
10830
11048
|
/** @property {Vector2} - Text offset for the button */
|
|
@@ -10865,7 +11083,7 @@ class UICheckbox extends UIObject
|
|
|
10865
11083
|
{
|
|
10866
11084
|
super(pos, size);
|
|
10867
11085
|
|
|
10868
|
-
ASSERT(
|
|
11086
|
+
ASSERT(isStringLike(text), 'ui checkbox must be a string');
|
|
10869
11087
|
ASSERT(isColor(color), 'ui checkbox color must be a color');
|
|
10870
11088
|
|
|
10871
11089
|
/** @property {boolean} - Current percentage value of this slider 0-1 */
|
|
@@ -10922,7 +11140,7 @@ class UISlider extends UIObject
|
|
|
10922
11140
|
super(pos, size);
|
|
10923
11141
|
|
|
10924
11142
|
ASSERT(isNumber(value), 'ui slider value must be a number');
|
|
10925
|
-
ASSERT(
|
|
11143
|
+
ASSERT(isStringLike(text), 'ui slider must be a string');
|
|
10926
11144
|
ASSERT(isColor(color), 'ui slider color must be a color');
|
|
10927
11145
|
ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
|
|
10928
11146
|
|
|
@@ -11040,7 +11258,7 @@ class UIVideo extends UIObject
|
|
|
11040
11258
|
{
|
|
11041
11259
|
super(pos, size || vec2());
|
|
11042
11260
|
|
|
11043
|
-
ASSERT(
|
|
11261
|
+
ASSERT(isStringLike(src), 'video src must be a string');
|
|
11044
11262
|
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
11045
11263
|
|
|
11046
11264
|
this.color = BLACK; // default to black background
|
|
@@ -13544,13 +13762,21 @@ class Tween
|
|
|
13544
13762
|
}
|
|
13545
13763
|
ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
|
|
13546
13764
|
|
|
13765
|
+
/** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
|
|
13547
13766
|
this.callback = callback;
|
|
13767
|
+
/** @property {number|Vector2|Color} - Starting value */
|
|
13548
13768
|
this.start = start;
|
|
13769
|
+
/** @property {number|Vector2|Color} - Ending value */
|
|
13549
13770
|
this.end = end;
|
|
13771
|
+
/** @property {number} - Total duration in seconds */
|
|
13550
13772
|
this.duration = duration;
|
|
13773
|
+
/** @property {number} - Remaining time in seconds (counts down from duration to 0) */
|
|
13551
13774
|
this.life = duration;
|
|
13775
|
+
/** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
|
|
13552
13776
|
this.ease = options.ease || Ease.LINEAR;
|
|
13777
|
+
/** @property {boolean} - If true, advance even when the game is paused */
|
|
13553
13778
|
this.useRealTime = !!options.useRealTime;
|
|
13779
|
+
/** @property {boolean} - If true, stop advancing until cleared */
|
|
13554
13780
|
this.paused = !!options.paused;
|
|
13555
13781
|
|
|
13556
13782
|
/** @private completion callback set by then(), loop(), pingPong(). */
|
|
@@ -13890,7 +14116,7 @@ const Ease =
|
|
|
13890
14116
|
function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
|
|
13891
14117
|
{
|
|
13892
14118
|
ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
|
|
13893
|
-
ASSERT(
|
|
14119
|
+
ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
|
|
13894
14120
|
|
|
13895
14121
|
const parts = propertyPath.split('.');
|
|
13896
14122
|
const lastKey = parts.pop();
|
|
@@ -14083,7 +14309,9 @@ class PathFinder
|
|
|
14083
14309
|
// .size + .getCollisionData.
|
|
14084
14310
|
if (isVector2(source))
|
|
14085
14311
|
{
|
|
14312
|
+
/** @property {Vector2} - Grid dimensions in tiles */
|
|
14086
14313
|
this.size = source.floor();
|
|
14314
|
+
/** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
|
|
14087
14315
|
this.tileLayer = undefined;
|
|
14088
14316
|
}
|
|
14089
14317
|
else
|
|
@@ -14095,13 +14323,18 @@ class PathFinder
|
|
|
14095
14323
|
}
|
|
14096
14324
|
|
|
14097
14325
|
// Tunables (public, freely re-assignable).
|
|
14326
|
+
/** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
|
|
14098
14327
|
this.heuristicWeight = 1;
|
|
14099
|
-
|
|
14328
|
+
/** @property {number} - Maximum A* expansions before giving up */
|
|
14329
|
+
this.maxLoop = 1e3;
|
|
14330
|
+
/** @property {boolean} - If true, post-process paths with two-pass smoothing */
|
|
14100
14331
|
this.smoothPath = true;
|
|
14332
|
+
/** @property {boolean} - If true, draw debug visualization during findPath */
|
|
14101
14333
|
this.debug = false;
|
|
14102
|
-
|
|
14334
|
+
/** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
|
|
14335
|
+
this.debugTime = 1;
|
|
14103
14336
|
|
|
14104
|
-
|
|
14337
|
+
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
|
|
14105
14338
|
this.nodes = new Array(this.size.x * this.size.y);
|
|
14106
14339
|
for (let y = 0; y < this.size.y; ++y)
|
|
14107
14340
|
for (let x = 0; x < this.size.x; ++x)
|
|
@@ -14275,9 +14508,12 @@ class PathFinder
|
|
|
14275
14508
|
// Best path so far through neighbor — record it.
|
|
14276
14509
|
neighbor.parent = current;
|
|
14277
14510
|
neighbor.g = tentativeG;
|
|
14278
|
-
|
|
14279
|
-
|
|
14280
|
-
|
|
14511
|
+
// Octile heuristic — tightest admissible distance for an
|
|
14512
|
+
// 8-connected grid with cardinal cost 1 and diagonal cost √2.
|
|
14513
|
+
const adx = abs(endNode.pos.x - neighbor.pos.x);
|
|
14514
|
+
const ady = abs(endNode.pos.y - neighbor.pos.y);
|
|
14515
|
+
const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
|
|
14516
|
+
neighbor.f = neighbor.g + h * this.heuristicWeight;
|
|
14281
14517
|
}
|
|
14282
14518
|
}
|
|
14283
14519
|
|
|
@@ -14559,6 +14795,24 @@ class PathFinder
|
|
|
14559
14795
|
path.push(original[original.length - 1]);
|
|
14560
14796
|
}
|
|
14561
14797
|
|
|
14798
|
+
/** Drop any middle node that lies exactly on the line through its two
|
|
14799
|
+
* neighbors. Backstop for the smoothing passes — the corners pass
|
|
14800
|
+
* intentionally keeps truly-straight runs, and the string-pulling pass
|
|
14801
|
+
* checks collinearity against the original path, not the in-progress
|
|
14802
|
+
* result, so it can leave 3+ collinear nodes in some edge cases.
|
|
14803
|
+
* @param {PathFinderNode[]} path
|
|
14804
|
+
* @private */
|
|
14805
|
+
dropCollinearNodes(path)
|
|
14806
|
+
{
|
|
14807
|
+
for (let i = path.length - 2; i >= 1; --i)
|
|
14808
|
+
{
|
|
14809
|
+
const a = path[i - 1], b = path[i], c = path[i + 1];
|
|
14810
|
+
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
|
|
14811
|
+
(b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
|
|
14812
|
+
path.splice(i, 1);
|
|
14813
|
+
}
|
|
14814
|
+
}
|
|
14815
|
+
|
|
14562
14816
|
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
14563
14817
|
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
14564
14818
|
* @param {number} x
|
|
@@ -14726,6 +14980,7 @@ class PathFinder
|
|
|
14726
14980
|
{
|
|
14727
14981
|
this.smoothPathCorners(nodePath);
|
|
14728
14982
|
this.smoothPathStringPull(nodePath);
|
|
14983
|
+
this.dropCollinearNodes(nodePath);
|
|
14729
14984
|
}
|
|
14730
14985
|
|
|
14731
14986
|
// Convert to world-space Vector2 path. Return copies, not live node
|