littlejsengine 1.25.0 → 1.26.1
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/CHANGELOG.md +99 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +129 -21
- package/dist/littlejs.d.ts +487 -102
- package/dist/littlejs.esm.js +2898 -719
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2888 -716
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2702 -527
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +9 -3
- package/plugins/render3d.js +499 -2389
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +199 -40
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +142 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +1 -3
- package/src/engineInput.js +34 -24
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
|
@@ -88,9 +88,11 @@ class VoxelMap extends EngineObject3D
|
|
|
88
88
|
this.chunkTransparentMeshes = []; // each chunk's transparent blocks, drawn blended
|
|
89
89
|
/** @type {Array<Vector3>} */
|
|
90
90
|
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
91
|
+
/** @type {Set<number>} */
|
|
91
92
|
this.chunksChanged = new Set;
|
|
92
93
|
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
|
|
93
94
|
this.blockTypes = [];
|
|
95
|
+
/** @type {Map<number, Array<Vector2>>} */
|
|
94
96
|
this.tiles = new Map; // tile index to the uvs of its corners
|
|
95
97
|
|
|
96
98
|
// the transparent blocks draw in the transparent stage, blended and sorted, through a child that draws them
|
|
@@ -98,6 +100,7 @@ class VoxelMap extends EngineObject3D
|
|
|
98
100
|
glass.size3D = vec3(); // not a thing to collect either
|
|
99
101
|
glass.transparent = true;
|
|
100
102
|
glass.render3D = ()=> this.renderChunks(true);
|
|
103
|
+
render3DShareSettings(glass, this); // it draws as the map does, its material, stage and render order
|
|
101
104
|
this.addChild(glass);
|
|
102
105
|
render3DLevel.push(this);
|
|
103
106
|
}
|
|
@@ -473,15 +476,24 @@ class VoxelMap extends EngineObject3D
|
|
|
473
476
|
levelRaycast3D(ray) { return this.raycast(ray)?.distance; }
|
|
474
477
|
|
|
475
478
|
/** Where a short move goes into a block, for a particle's move in one frame: the part of the move made before it,
|
|
476
|
-
* 0 to 1, and the normal of the face it comes in through; undefined when it hits none, or starts inside one
|
|
479
|
+
* 0 to 1, and the normal of the face it comes in through; undefined when it hits none, or starts inside one; a
|
|
480
|
+
* move that starts on a block's face, coming in, hits it at 0
|
|
477
481
|
* @param {Vector3} from
|
|
478
482
|
* @param {Vector3} to
|
|
479
483
|
* @return {{distance: number, normal: Vector3}|undefined}
|
|
480
484
|
* @ignore */
|
|
481
485
|
levelSegment3D(from, to)
|
|
482
486
|
{
|
|
483
|
-
const hit = this.raycast(new Ray3D(from,
|
|
484
|
-
|
|
487
|
+
const move = to.subtract(from), hit = this.raycast(new Ray3D(from, move), 1);
|
|
488
|
+
if (!hit) return;
|
|
489
|
+
if (!hit.distance)
|
|
490
|
+
{
|
|
491
|
+
// met at its start: on a face when the point just behind the start is clear, inside a block when not
|
|
492
|
+
const behind = from.subtract(move.normalize(1e-6)).subtract(this.pos3D);
|
|
493
|
+
if (this.voxelAt(floor(behind.x), floor(behind.y), floor(behind.z)))
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
return {distance: hit.distance, normal: hit.normal};
|
|
485
497
|
}
|
|
486
498
|
|
|
487
499
|
/** Keeps an eye on its placement, called automatically each frame */
|
package/plugins/textureSheet.js
CHANGED
|
@@ -263,7 +263,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
263
263
|
console.warn('loadSprite failed to load image:', src);
|
|
264
264
|
textureSheetLoadFailed(key, tileInfo);
|
|
265
265
|
}
|
|
266
|
-
});
|
|
266
|
+
}, ()=> textureSheetLoadFailed(key, tileInfo));
|
|
267
267
|
|
|
268
268
|
return tileInfo;
|
|
269
269
|
}
|
|
@@ -329,7 +329,7 @@ function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPaddin
|
|
|
329
329
|
added.sheet.drawImage(image, added.tile, false); // upload once per batch
|
|
330
330
|
for (let k = 0; k < count; ++k)
|
|
331
331
|
set.tiles.push(added.tile.frame(k));
|
|
332
|
-
});
|
|
332
|
+
}, ()=> textureSheetLoadFailed(key, set));
|
|
333
333
|
}
|
|
334
334
|
return set;
|
|
335
335
|
}
|
|
@@ -434,20 +434,21 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
434
434
|
console.warn('loadAtlas failed to load:', imageSrc, jsonSrc);
|
|
435
435
|
textureSheetLoadFailed(key, atlas);
|
|
436
436
|
}
|
|
437
|
-
});
|
|
437
|
+
}, ()=> textureSheetLoadFailed(key, atlas));
|
|
438
438
|
|
|
439
439
|
return atlas;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
// run a load in the queue: a load that throws is reported
|
|
443
|
-
//
|
|
444
|
-
|
|
442
|
+
// run a load in the queue: a load that throws is reported, let go of so the next call loads it again (failed), as
|
|
443
|
+
// one whose file did not load is, and the loads after it carry on; the pending count always comes back down, and the
|
|
444
|
+
// sheets upload to webgl once per batch, when the last pending load finishes
|
|
445
|
+
function textureSheetQueueJob(name, job, failed)
|
|
445
446
|
{
|
|
446
447
|
++textureSheetPendingCount;
|
|
447
448
|
textureSheetQueue = textureSheetQueue.then(async ()=>
|
|
448
449
|
{
|
|
449
450
|
try { await job(); }
|
|
450
|
-
catch (e) { console.error(name + ' failed:', e); }
|
|
451
|
+
catch (e) { console.error(name + ' failed:', e); failed?.(); }
|
|
451
452
|
finally
|
|
452
453
|
{
|
|
453
454
|
if (!--textureSheetPendingCount)
|
|
@@ -464,7 +465,7 @@ function textureSheetQueueJob(name, job)
|
|
|
464
465
|
* - Accepts TexturePacker json (hash and array) and Aseprite json
|
|
465
466
|
* - Frames tagged in Aseprite or named like run_0, run_1 group into animations
|
|
466
467
|
* @param {Object} data - Parsed atlas json data
|
|
467
|
-
* @return {Array<Object>} List of {name, frames} groups in atlas order
|
|
468
|
+
* @return {Array<{name: string, frames: Array<Object>}>} List of {name, frames} groups in atlas order
|
|
468
469
|
* @memberof TextureSheets */
|
|
469
470
|
function parseAtlas(data)
|
|
470
471
|
{
|
package/plugins/threejs.js
CHANGED
|
@@ -65,7 +65,7 @@ class ThreeJSPlugin
|
|
|
65
65
|
rootElement.insertBefore(threeCanvas, rootElement.firstChild);
|
|
66
66
|
threeCanvas.style.cssText = mainCanvas.style.cssText;
|
|
67
67
|
|
|
68
|
-
// composite the 3D canvas into screenshots
|
|
68
|
+
// composite the 3D canvas into screenshots
|
|
69
69
|
setBackgroundCanvas(threeCanvas);
|
|
70
70
|
|
|
71
71
|
// render automatically each frame after the engine renders
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -28,6 +28,20 @@ function tweenActivate(tween)
|
|
|
28
28
|
}
|
|
29
29
|
function tweenDeactivate(tween) { tween.active = false; }
|
|
30
30
|
|
|
31
|
+
// call a tween's callback with its value at life; one that throws, its easing too, is ended, or it would be called and
|
|
32
|
+
// throw at every update, unless the callback itself started it again, which is its own new run (pass, the update it
|
|
33
|
+
// is in, undefined outside one)
|
|
34
|
+
function tweenCall(tween, life, pass)
|
|
35
|
+
{
|
|
36
|
+
try { tween.callback(tween.interp(life)); }
|
|
37
|
+
catch (error)
|
|
38
|
+
{
|
|
39
|
+
if (tween.active && (pass === undefined || tween.activePass < pass))
|
|
40
|
+
tweenDeactivate(tween);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
// True if the value is an instance of a class that exposes a numeric-percent
|
|
32
46
|
// `lerp(other, percent)` method (Vector2, Color, or any future class).
|
|
33
47
|
function tweenIsLerpable(v) { return v && typeof v.lerp === 'function'; }
|
|
@@ -102,10 +116,10 @@ class Tween
|
|
|
102
116
|
* @type {undefined|function():void} */
|
|
103
117
|
this.onComplete = undefined;
|
|
104
118
|
|
|
105
|
-
/** Continuation when a pass ends, set by loop() and pingPong() to start the next iteration
|
|
119
|
+
/** Continuation when a pass ends, set by loop() and pingPong() to start the next iteration
|
|
106
120
|
* @private */
|
|
107
121
|
this.thenCallback = undefined;
|
|
108
|
-
/** Remaining iterations including the current run (loop/pingPong only)
|
|
122
|
+
/** Remaining iterations including the current run (loop/pingPong only)
|
|
109
123
|
* @private */
|
|
110
124
|
this.loopRemaining = 0;
|
|
111
125
|
/** Whether it is running, see isActive
|
|
@@ -128,11 +142,11 @@ class Tween
|
|
|
128
142
|
this.target = undefined;
|
|
129
143
|
|
|
130
144
|
tweenActivate(this);
|
|
131
|
-
// Snap target to start immediately
|
|
132
|
-
|
|
145
|
+
// Snap target to start immediately; one that throws here is no tween, the game never gets it to stop
|
|
146
|
+
tweenCall(this, duration);
|
|
133
147
|
}
|
|
134
148
|
|
|
135
|
-
/** Set the easing curve and return this for chaining
|
|
149
|
+
/** Set the easing curve and return this for chaining
|
|
136
150
|
* @param {function(number):number} easeFn
|
|
137
151
|
* @return {Tween<T>} */
|
|
138
152
|
setEase(easeFn)
|
|
@@ -188,10 +202,10 @@ class Tween
|
|
|
188
202
|
return this;
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
/** Pause this tween. While paused, tweenUpdate skips it
|
|
205
|
+
/** Pause this tween. While paused, tweenUpdate skips it */
|
|
192
206
|
pause() { this.paused = true; }
|
|
193
207
|
|
|
194
|
-
/** Resume a paused tween
|
|
208
|
+
/** Resume a paused tween */
|
|
195
209
|
resume() { this.paused = false; }
|
|
196
210
|
|
|
197
211
|
/** Reset this tween to the start: life back to duration, pause cleared,
|
|
@@ -209,10 +223,10 @@ class Tween
|
|
|
209
223
|
this.lastTime = time;
|
|
210
224
|
this.lastTimeReal = timeReal;
|
|
211
225
|
tweenActivate(this);
|
|
212
|
-
|
|
226
|
+
tweenCall(this, this.duration);
|
|
213
227
|
}
|
|
214
228
|
|
|
215
|
-
/** True if this tween is in the active list and not paused
|
|
229
|
+
/** True if this tween is in the active list and not paused
|
|
216
230
|
* @return {boolean} */
|
|
217
231
|
isActive()
|
|
218
232
|
{
|
|
@@ -283,7 +297,7 @@ class Tween
|
|
|
283
297
|
*/
|
|
284
298
|
const Ease =
|
|
285
299
|
{
|
|
286
|
-
/** Linear (identity) curve
|
|
300
|
+
/** Linear (identity) curve
|
|
287
301
|
* @param {number} x
|
|
288
302
|
* @return {number}
|
|
289
303
|
* @memberof TweenSystem.Ease */
|
|
@@ -296,31 +310,31 @@ const Ease =
|
|
|
296
310
|
* @memberof TweenSystem.Ease */
|
|
297
311
|
POWER: (n) => (x) => x ** n,
|
|
298
312
|
|
|
299
|
-
/** Sine ease-in curve: starts slow, ends fast
|
|
313
|
+
/** Sine ease-in curve: starts slow, ends fast
|
|
300
314
|
* @param {number} x
|
|
301
315
|
* @return {number}
|
|
302
316
|
* @memberof TweenSystem.Ease */
|
|
303
317
|
SINE: (x) => 1 - cos(x * (PI / 2)),
|
|
304
318
|
|
|
305
|
-
/** Circular ease-in curve
|
|
319
|
+
/** Circular ease-in curve
|
|
306
320
|
* @param {number} x
|
|
307
321
|
* @return {number}
|
|
308
322
|
* @memberof TweenSystem.Ease */
|
|
309
323
|
CIRC: (x) => 1 - (1 - x * x)**.5,
|
|
310
324
|
|
|
311
|
-
/** Exponential ease-in curve (`2^(10x-10)`)
|
|
325
|
+
/** Exponential ease-in curve (`2^(10x-10)`)
|
|
312
326
|
* @param {number} x
|
|
313
327
|
* @return {number}
|
|
314
328
|
* @memberof TweenSystem.Ease */
|
|
315
329
|
EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
|
|
316
330
|
|
|
317
|
-
/** Back ease-in: overshoots backward at the start before snapping forward
|
|
331
|
+
/** Back ease-in: overshoots backward at the start before snapping forward
|
|
318
332
|
* @param {number} x
|
|
319
333
|
* @return {number}
|
|
320
334
|
* @memberof TweenSystem.Ease */
|
|
321
335
|
BACK: (x) => x * x * (2.70158 * x - 1.70158),
|
|
322
336
|
|
|
323
|
-
/** Elastic ease-in: oscillations that grow toward the end
|
|
337
|
+
/** Elastic ease-in: oscillations that grow toward the end
|
|
324
338
|
* @param {number} x
|
|
325
339
|
* @return {number}
|
|
326
340
|
* @memberof TweenSystem.Ease */
|
|
@@ -387,7 +401,7 @@ const Ease =
|
|
|
387
401
|
*/
|
|
388
402
|
OUT: (f) => (x) => 1 - f(1 - x),
|
|
389
403
|
|
|
390
|
-
/** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve
|
|
404
|
+
/** Combine the first half of `f` with `Ease.OUT(f)` for a symmetric curve
|
|
391
405
|
* @param {function(number):number} f
|
|
392
406
|
* @return {function(number):number}
|
|
393
407
|
* @memberof TweenSystem.Ease */
|
|
@@ -527,7 +541,7 @@ function tweenNextIteration(tween, passed, continuation)
|
|
|
527
541
|
tweenCarryOvershoot(tween);
|
|
528
542
|
tween.thenCallback = continuation;
|
|
529
543
|
tweenActivate(tween);
|
|
530
|
-
|
|
544
|
+
tweenCall(tween, tween.life); // snap to where the new iteration is
|
|
531
545
|
return true;
|
|
532
546
|
}
|
|
533
547
|
|
|
@@ -600,48 +614,59 @@ function tweenUpdate(gameDelta, realDelta)
|
|
|
600
614
|
tweenActive.length = kept;
|
|
601
615
|
// the list is let go however the walk ends: a callback that throws must not leave it held, or every
|
|
602
616
|
// update after would take it for an update still going and make a list of its own
|
|
617
|
+
let failed;
|
|
603
618
|
try
|
|
604
619
|
{
|
|
605
620
|
for (let i = list.length; i--;)
|
|
606
621
|
{
|
|
607
622
|
const t = list[i];
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
let dt;
|
|
612
|
-
if (enginePath)
|
|
613
|
-
{
|
|
614
|
-
// a paused tween keeps count too, so it does not jump when resumed
|
|
615
|
-
dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
|
|
616
|
-
t.lastTime = time;
|
|
617
|
-
t.lastTimeReal = timeReal;
|
|
618
|
-
}
|
|
619
|
-
else
|
|
620
|
-
dt = t.useRealTime ? realDelta : gameDelta;
|
|
621
|
-
if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
|
|
622
|
-
if (t.paused || dt <= 0) continue;
|
|
623
|
-
|
|
624
|
-
t.life -= dt;
|
|
625
|
-
if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
|
|
626
|
-
{
|
|
627
|
-
t.callback(t.interp(t.life));
|
|
628
|
-
}
|
|
629
|
-
else
|
|
630
|
-
{
|
|
631
|
-
// Completion: fire end value, remove from active, start the next iteration
|
|
632
|
-
// of a loop or pingPong, or when there is none it has completed, fire onComplete
|
|
633
|
-
t.callback(t.interp(0));
|
|
634
|
-
if (!t.active || t.activePass >= pass)
|
|
635
|
-
continue; // stopped or restarted by its own callback, the run it was on ends without completing
|
|
636
|
-
tweenDeactivate(t);
|
|
637
|
-
const next = t.thenCallback;
|
|
638
|
-
t.thenCallback = undefined;
|
|
639
|
-
if (!(next && next()) && t.onComplete)
|
|
640
|
-
t.onComplete();
|
|
641
|
-
}
|
|
623
|
+
// one that throws does not stop the others: its error comes out once they have all moved
|
|
624
|
+
try { tweenStep(t, pass, enginePath, gameDelta, realDelta); }
|
|
625
|
+
catch (error) { failed ??= {error}; }
|
|
642
626
|
}
|
|
643
627
|
}
|
|
644
628
|
finally { list.length = 0; }
|
|
629
|
+
if (failed)
|
|
630
|
+
throw failed.error;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// move one tween by an update, as tweenUpdate walks them
|
|
634
|
+
function tweenStep(t, pass, enginePath, gameDelta, realDelta)
|
|
635
|
+
{
|
|
636
|
+
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
637
|
+
// counts on from this one
|
|
638
|
+
if (!t.active || t.activePass >= pass) return;
|
|
639
|
+
let dt;
|
|
640
|
+
if (enginePath)
|
|
641
|
+
{
|
|
642
|
+
// a paused tween keeps count too, so it does not jump when resumed
|
|
643
|
+
dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
|
|
644
|
+
t.lastTime = time;
|
|
645
|
+
t.lastTimeReal = timeReal;
|
|
646
|
+
}
|
|
647
|
+
else
|
|
648
|
+
dt = t.useRealTime ? realDelta : gameDelta;
|
|
649
|
+
if (t.target?.destroyed) { t.stop(); return; } // its object is gone, paused or not
|
|
650
|
+
if (t.paused || dt <= 0) return;
|
|
651
|
+
|
|
652
|
+
t.life -= dt;
|
|
653
|
+
if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
|
|
654
|
+
{
|
|
655
|
+
tweenCall(t, t.life, pass);
|
|
656
|
+
}
|
|
657
|
+
else
|
|
658
|
+
{
|
|
659
|
+
// Completion: fire end value, remove from active, start the next iteration
|
|
660
|
+
// of a loop or pingPong, or when there is none it has completed, fire onComplete
|
|
661
|
+
tweenCall(t, 0, pass); // one whose last call throws is ended all the same
|
|
662
|
+
if (!t.active || t.activePass >= pass)
|
|
663
|
+
return; // stopped or restarted by its own callback, the run it was on ends without completing
|
|
664
|
+
tweenDeactivate(t);
|
|
665
|
+
const next = t.thenCallback;
|
|
666
|
+
t.thenCallback = undefined;
|
|
667
|
+
if (!(next && next()) && t.onComplete)
|
|
668
|
+
t.onComplete();
|
|
669
|
+
}
|
|
645
670
|
}
|
|
646
671
|
|
|
647
672
|
/** Stop every active tween, ending loops too, without calling their then-callbacks.
|
package/plugins/uiSystem.js
CHANGED
|
@@ -28,6 +28,10 @@ let uiSystem;
|
|
|
28
28
|
* @memberof UISystem */
|
|
29
29
|
let uiDebug = 0;
|
|
30
30
|
|
|
31
|
+
// the active object's press was let go of in the frame it came, a tap, so it is clicked on the next with no release
|
|
32
|
+
// then; a click needs a release, which the mouse let go of by a window losing focus is not
|
|
33
|
+
let uiActiveReleased = false;
|
|
34
|
+
|
|
31
35
|
/** Enable UI system debug drawing
|
|
32
36
|
* 0=off, 1=normal, 2=show invisible
|
|
33
37
|
* @param {number|boolean} debugMode
|
|
@@ -199,8 +203,10 @@ class UISystemPlugin
|
|
|
199
203
|
|
|
200
204
|
// setup recursive update and render
|
|
201
205
|
// update in reverse order to detect mouse enter/leave
|
|
206
|
+
let updatePass = 0; // marks the objects each update has reached, see updateObject
|
|
202
207
|
function uiUpdate()
|
|
203
208
|
{
|
|
209
|
+
++updatePass;
|
|
204
210
|
// a held or focused object that can no longer be used, itself or through a parent, lets go,
|
|
205
211
|
// and one that was hidden or disabled is still released, a destroyed one stays silent
|
|
206
212
|
const activeObject = uiSystem.activeObject;
|
|
@@ -345,7 +351,10 @@ class UISystemPlugin
|
|
|
345
351
|
|
|
346
352
|
function updateObject(o)
|
|
347
353
|
{
|
|
348
|
-
|
|
354
|
+
// once a pass, though a callback may move it under a parent not yet reached, or detach it to the
|
|
355
|
+
// top level, where the loop reaches it again
|
|
356
|
+
if (o.destroyed || !o.visible || o.uiUpdatePass === updatePass) return;
|
|
357
|
+
o.uiUpdatePass = updatePass;
|
|
349
358
|
|
|
350
359
|
// update in reverse order to detect mouse enter/leave, from a copy since a child may destroy
|
|
351
360
|
// siblings mid-update (e.g. dialog close) and the ones after it would shift under the loop
|
|
@@ -821,7 +830,7 @@ class UISystemPlugin
|
|
|
821
830
|
// close menu and clear the input that closed it
|
|
822
831
|
function closeMenu()
|
|
823
832
|
{
|
|
824
|
-
ASSERT(uiSystem.confirmDialog === confirmMenu);
|
|
833
|
+
ASSERT(uiSystem.confirmDialog === confirmMenu, 'the confirm dialog closing is not the one open');
|
|
825
834
|
confirmMenu.destroy();
|
|
826
835
|
inputClear();
|
|
827
836
|
}
|
|
@@ -961,6 +970,7 @@ class UIObject
|
|
|
961
970
|
this.textLineWidth = 0;
|
|
962
971
|
/** @property {boolean} - Should this object be drawn */
|
|
963
972
|
this.visible = true;
|
|
973
|
+
this.uiUpdatePass = 0; // the UI update that last reached it, so a pass updates it once
|
|
964
974
|
/** @property {Array<UIObject>} - A list of this object's children
|
|
965
975
|
* @type {Array<UIObject>} */
|
|
966
976
|
this.children = [];
|
|
@@ -1023,7 +1033,8 @@ class UIObject
|
|
|
1023
1033
|
* @param {UIObject} child */
|
|
1024
1034
|
removeChild(child)
|
|
1025
1035
|
{
|
|
1026
|
-
ASSERT(child.parent === this && this.children.includes(child)
|
|
1036
|
+
ASSERT(child.parent === this && this.children.includes(child), 'removeChild: that object is not a child of this one',
|
|
1037
|
+
child);
|
|
1027
1038
|
this.children.splice(this.children.indexOf(child), 1);
|
|
1028
1039
|
child.parent = undefined;
|
|
1029
1040
|
}
|
|
@@ -1095,10 +1106,24 @@ class UIObject
|
|
|
1095
1106
|
return;
|
|
1096
1107
|
|
|
1097
1108
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
1098
|
-
|
|
1109
|
+
let isActive = this.isActiveObject();
|
|
1099
1110
|
const mouseDown = mouseIsDown(0);
|
|
1100
1111
|
// a press and release in one frame is a press too, as it is for a button that is not drag activated
|
|
1101
1112
|
const mousePress = mouseWasPressed(0) || this.dragActivate && mouseDown;
|
|
1113
|
+
|
|
1114
|
+
// a new press while this is still active from the one before means that one was let go of in between, by a
|
|
1115
|
+
// second quick tap or a release missed in a hitch: it is clicked and released first, so neither is lost
|
|
1116
|
+
if (isActive && mouseWasPressed(0))
|
|
1117
|
+
{
|
|
1118
|
+
if (!uiSystem.activateOnPress && this.interactive && !disabled)
|
|
1119
|
+
this.click();
|
|
1120
|
+
if (this.destroyed) return;
|
|
1121
|
+
uiSystem.activeObject = undefined;
|
|
1122
|
+
isActive = false;
|
|
1123
|
+
this.onRelease();
|
|
1124
|
+
this.soundRelease && this.soundRelease.play();
|
|
1125
|
+
if (this.destroyed) return;
|
|
1126
|
+
}
|
|
1102
1127
|
if (this.canBeHover)
|
|
1103
1128
|
if (!uiSystem.navigationMode) // no mouse hover in navigation mode
|
|
1104
1129
|
if (mousePress || isActive || (!mouseDown && !isTouchDevice))
|
|
@@ -1130,7 +1155,10 @@ class UIObject
|
|
|
1130
1155
|
this.soundRelease && this.soundRelease.play();
|
|
1131
1156
|
}
|
|
1132
1157
|
else
|
|
1158
|
+
{
|
|
1133
1159
|
uiSystem.activeObject = this;
|
|
1160
|
+
uiActiveReleased = !mouseDown;
|
|
1161
|
+
}
|
|
1134
1162
|
|
|
1135
1163
|
if (newPress && uiSystem.activateOnPress)
|
|
1136
1164
|
this.click(!this.soundPress);
|
|
@@ -1142,6 +1170,7 @@ class UIObject
|
|
|
1142
1170
|
// frame is clicked on the next, with its release, not on both
|
|
1143
1171
|
if (!uiSystem.activateOnPress)
|
|
1144
1172
|
if (!mouseDown && isActive && this.isActiveObject() && this.interactive)
|
|
1173
|
+
if (mouseWasReleased(0) || uiActiveReleased)
|
|
1145
1174
|
this.click();
|
|
1146
1175
|
if (this.destroyed) return;
|
|
1147
1176
|
}
|
|
@@ -1194,7 +1223,7 @@ class UIObject
|
|
|
1194
1223
|
getTextSize()
|
|
1195
1224
|
{
|
|
1196
1225
|
// text fitted to the size shares its height between its lines, a set textHeight is the height of each line
|
|
1197
|
-
const lines = this.textHeight ? 1 : (this.text + '')
|
|
1226
|
+
const lines = this.textHeight ? 1 : textLineCount(this.text + '');
|
|
1198
1227
|
return vec2(
|
|
1199
1228
|
this.textWidth || this.textFitScale * this.size.x,
|
|
1200
1229
|
this.textHeight || this.textFitScale * this.size.y / lines);
|
|
@@ -1376,7 +1405,7 @@ class UITextInput extends UIObject
|
|
|
1376
1405
|
|
|
1377
1406
|
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
1378
1407
|
|
|
1379
|
-
/** @property {number} - Max length of input (0 = no limit) */
|
|
1408
|
+
/** @property {number} - Max length of input in characters as a reader counts them (0 = no limit) */
|
|
1380
1409
|
this.maxLength = 0;
|
|
1381
1410
|
|
|
1382
1411
|
// set properties, as a string, which typing adds to
|
|
@@ -1424,7 +1453,7 @@ class UITextInput extends UIObject
|
|
|
1424
1453
|
return; // a key held when editing began repeats, it should not type or stop editing
|
|
1425
1454
|
this.text += ''; // a game may have set a number
|
|
1426
1455
|
if (key === 'Backspace')
|
|
1427
|
-
this.text =
|
|
1456
|
+
this.text = textGraphemes(this.text).slice(0, -1).join(''); // a whole character, an emoji family too
|
|
1428
1457
|
else if (key === 'Enter' || key === 'Escape')
|
|
1429
1458
|
this.stopEditing();
|
|
1430
1459
|
else if (key.length === 1) // printable characters
|
|
@@ -1432,7 +1461,7 @@ class UITextInput extends UIObject
|
|
|
1432
1461
|
// ctrl and cmd shortcuts do not type, but AltGr reports ctrl and alt and types characters like @
|
|
1433
1462
|
if ((e.ctrlKey || e.metaKey) && !e.getModifierState?.('AltGraph'))
|
|
1434
1463
|
return;
|
|
1435
|
-
if (!this.maxLength || this.text.length < this.maxLength)
|
|
1464
|
+
if (!this.maxLength || textGraphemes(this.text).length < this.maxLength)
|
|
1436
1465
|
this.text += key;
|
|
1437
1466
|
}
|
|
1438
1467
|
}
|