littlejsengine 1.18.12 → 1.18.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +197 -15
- package/README.md +28 -7
- package/dist/littlejs.d.ts +105 -77
- package/dist/littlejs.esm.js +414 -277
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +406 -272
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +403 -265
- package/package.json +4 -1
- package/plugins/box2d.js +17 -6
- package/plugins/newgrounds.js +15 -3
- package/plugins/pathFinder.js +5 -3
- package/plugins/pluginExport.js +4 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +25 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +14 -3
- package/src/engineAudio.js +27 -13
- package/src/engineBuild.mjs +1 -1
- package/src/engineDebug.js +3 -7
- package/src/engineDraw.js +71 -40
- package/src/engineExport.js +4 -2
- package/src/engineInput.js +17 -12
- package/src/engineLogo.js +1 -1
- package/src/engineMath.js +21 -7
- package/src/engineObject.js +19 -11
- package/src/engineParticles.js +26 -27
- package/src/engineSettings.js +3 -2
- package/src/engineTileLayer.js +62 -43
- package/src/engineUtilities.js +24 -11
- package/src/engineWebGL.js +23 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.15",
|
|
4
4
|
"description": "LittleJS - Tiny and Fast HTML5 Game Engine",
|
|
5
5
|
"main": "dist/littlejs.esm.js",
|
|
6
6
|
"types": "dist/littlejs.d.ts",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"build": "node src/engineBuild.mjs",
|
|
41
41
|
"test": "node --test --import ./test/setup.mjs \"test/**/*.test.mjs\""
|
|
42
42
|
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
43
46
|
"devDependencies": {
|
|
44
47
|
"bestzip": "^2.2.1",
|
|
45
48
|
"electron": "^38.2.0",
|
package/plugins/box2d.js
CHANGED
|
@@ -83,6 +83,11 @@ class Box2dObject extends EngineObject
|
|
|
83
83
|
// destroy physics body, fixtures, and joints
|
|
84
84
|
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
85
85
|
box2d.world.DestroyBody(this.body);
|
|
86
|
+
|
|
87
|
+
// remove from tracked list so paused / headless sessions don't leak
|
|
88
|
+
const i = box2d.objects.indexOf(this);
|
|
89
|
+
if (i >= 0)
|
|
90
|
+
box2d.objects.splice(i, 1);
|
|
86
91
|
super.destroy();
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -171,7 +176,9 @@ class Box2dObject extends EngineObject
|
|
|
171
176
|
/** Add a box shape to the body
|
|
172
177
|
* @param {Vector2} [size]
|
|
173
178
|
* @param {Vector2} [offset]
|
|
174
|
-
* @param {number} [angle]
|
|
179
|
+
* @param {number} [angle] - LittleJS convention (clockwise positive).
|
|
180
|
+
* Negated internally to match Box2D's CCW-positive convention so the
|
|
181
|
+
* fixture aligns with the same angle passed to drawRect/drawTile.
|
|
175
182
|
* @param {number} [density]
|
|
176
183
|
* @param {number} [friction]
|
|
177
184
|
* @param {number} [restitution]
|
|
@@ -184,7 +191,7 @@ class Box2dObject extends EngineObject
|
|
|
184
191
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
185
192
|
|
|
186
193
|
const shape = new box2d.instance.b2PolygonShape();
|
|
187
|
-
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
194
|
+
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), -angle);
|
|
188
195
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
189
196
|
}
|
|
190
197
|
|
|
@@ -486,9 +493,10 @@ class Box2dObject extends EngineObject
|
|
|
486
493
|
{
|
|
487
494
|
const data = new box2d.instance.b2MassData();
|
|
488
495
|
this.body.GetMassData(data);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
496
|
+
// use !== undefined so setMass(0) (static-equivalent) isn't silently ignored
|
|
497
|
+
if (localCenter !== undefined) data.set_center(box2d.vec2dTo(localCenter));
|
|
498
|
+
if (mass !== undefined) data.set_mass(mass);
|
|
499
|
+
if (momentOfInertia !== undefined) data.set_I(momentOfInertia);
|
|
492
500
|
this.body.SetMassData(data);
|
|
493
501
|
}
|
|
494
502
|
|
|
@@ -1660,6 +1668,8 @@ class Box2dPlugin
|
|
|
1660
1668
|
const fixtureB = contact.GetFixtureB();
|
|
1661
1669
|
const objectA = fixtureA.GetBody().object;
|
|
1662
1670
|
const objectB = fixtureB.GetBody().object;
|
|
1671
|
+
// raw user-created b2Bodies may have no .object — skip those
|
|
1672
|
+
if (!objectA || !objectB) return;
|
|
1663
1673
|
objectA.beginContact(objectB);
|
|
1664
1674
|
objectB.beginContact(objectA);
|
|
1665
1675
|
}
|
|
@@ -1670,6 +1680,7 @@ class Box2dPlugin
|
|
|
1670
1680
|
const fixtureB = contact.GetFixtureB();
|
|
1671
1681
|
const objectA = fixtureA.GetBody().object;
|
|
1672
1682
|
const objectB = fixtureB.GetBody().object;
|
|
1683
|
+
if (!objectA || !objectB) return;
|
|
1673
1684
|
objectA.endContact(objectB);
|
|
1674
1685
|
objectB.endContact(objectA);
|
|
1675
1686
|
};
|
|
@@ -2048,7 +2059,7 @@ async function box2dInit()
|
|
|
2048
2059
|
debugDraw.DrawTransform = function(transform)
|
|
2049
2060
|
{
|
|
2050
2061
|
transform = box2d.instance.wrapPointer(transform, box2d.instance.b2Transform);
|
|
2051
|
-
const pos =
|
|
2062
|
+
const pos = box2d.vec2From(transform.get_p());
|
|
2052
2063
|
const angle = -transform.get_q().GetAngle();
|
|
2053
2064
|
const p1 = vec2(1,0), c1 = rgb(.75,0,0,.8);
|
|
2054
2065
|
const p2 = vec2(0,1), c2 = rgb(0,.75,0,.8);
|
package/plugins/newgrounds.js
CHANGED
|
@@ -82,8 +82,20 @@ class NewgroundsPlugin
|
|
|
82
82
|
|
|
83
83
|
// get medals
|
|
84
84
|
const medalsResult = this.call('Medal.getList');
|
|
85
|
+
|
|
86
|
+
// bail early if the first call failed (offline / bad session /
|
|
87
|
+
// server error) so we don't block the main thread on more sync
|
|
88
|
+
// XHRs that are guaranteed to also fail
|
|
89
|
+
if (!medalsResult || !medalsResult.result || medalsResult.result.error)
|
|
90
|
+
{
|
|
91
|
+
debugMedals && LOG('Newgrounds session unavailable; skipping plugin init');
|
|
92
|
+
this.medals = [];
|
|
93
|
+
this.scoreboards = [];
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
85
97
|
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
86
|
-
this.medals = medalsResult
|
|
98
|
+
this.medals = medalsResult.result.data?.['medals'] || [];
|
|
87
99
|
debugMedals && LOG(this.medals);
|
|
88
100
|
for (const newgroundsMedal of this.medals)
|
|
89
101
|
{
|
|
@@ -103,11 +115,11 @@ class NewgroundsPlugin
|
|
|
103
115
|
medal.description = medal.description + ` (${ medal.value })`;
|
|
104
116
|
}
|
|
105
117
|
}
|
|
106
|
-
|
|
118
|
+
|
|
107
119
|
// get scoreboards
|
|
108
120
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
109
121
|
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
110
|
-
this.scoreboards = scoreboardResult
|
|
122
|
+
this.scoreboards = scoreboardResult?.result?.data?.scoreboards || [];
|
|
111
123
|
debugMedals && LOG(this.scoreboards);
|
|
112
124
|
|
|
113
125
|
// keep the session alive with a ping every minute
|
package/plugins/pathFinder.js
CHANGED
|
@@ -271,11 +271,13 @@ class PathFinder
|
|
|
271
271
|
if (dx !== 0 && dy !== 0)
|
|
272
272
|
{
|
|
273
273
|
// Diagonal step: refuse if either cardinal neighbor is
|
|
274
|
-
// blocked
|
|
274
|
+
// blocked. Prevents cutting through walls at corners.
|
|
275
|
+
// (Costed-but-walkable cardinals do not block — diagonal
|
|
276
|
+
// movement around expensive terrain is standard A*.)
|
|
275
277
|
const card1 = this.getNode(current.pos.x + dx, current.pos.y);
|
|
276
|
-
if (!card1 ||
|
|
278
|
+
if (!card1 || !card1.walkable) continue;
|
|
277
279
|
const card2 = this.getNode(current.pos.x, current.pos.y + dy);
|
|
278
|
-
if (!card2 ||
|
|
280
|
+
if (!card2 || !card2.walkable) continue;
|
|
279
281
|
stepCost = PATHFINDER_DIAGONAL_COST;
|
|
280
282
|
}
|
|
281
283
|
|
package/plugins/pluginExport.js
CHANGED
|
@@ -7,16 +7,16 @@ export
|
|
|
7
7
|
// Medals
|
|
8
8
|
medals,
|
|
9
9
|
medalsPreventUnlock,
|
|
10
|
-
medalsInit,
|
|
11
|
-
medalsForEach,
|
|
12
|
-
Medal,
|
|
13
10
|
medalDisplayTime,
|
|
14
11
|
medalDisplaySlideTime,
|
|
15
12
|
medalDisplaySize,
|
|
13
|
+
medalsInit,
|
|
14
|
+
medalsForEach,
|
|
16
15
|
setMedalDisplayTime,
|
|
17
16
|
setMedalDisplaySlideTime,
|
|
18
17
|
setMedalDisplaySize,
|
|
19
18
|
setMedalsPreventUnlock,
|
|
19
|
+
Medal,
|
|
20
20
|
|
|
21
21
|
// Newgrounds
|
|
22
22
|
newgrounds,
|
|
@@ -29,6 +29,7 @@ export
|
|
|
29
29
|
|
|
30
30
|
// ZzFXMusic
|
|
31
31
|
ZzFXMusic,
|
|
32
|
+
zzfxM,
|
|
32
33
|
|
|
33
34
|
// UI System
|
|
34
35
|
uiSystem,
|
package/plugins/postProcess.js
CHANGED
|
@@ -107,10 +107,14 @@ class PostProcessPlugin
|
|
|
107
107
|
function postProcessRender()
|
|
108
108
|
{
|
|
109
109
|
if (headlessMode || !glEnable) return;
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
// clear out the buffer
|
|
112
112
|
glFlush();
|
|
113
113
|
|
|
114
|
+
// ensure we render to the default framebuffer (in case any earlier
|
|
115
|
+
// caller this frame left a render target bound)
|
|
116
|
+
glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
|
|
117
|
+
|
|
114
118
|
// setup shader program to draw a quad
|
|
115
119
|
glContext.useProgram(postProcess.shader);
|
|
116
120
|
glContext.bindVertexArray(postProcess.vao);
|
|
@@ -151,6 +155,9 @@ class PostProcessPlugin
|
|
|
151
155
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, glCanvas);
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
// restore default so subsequent dynamic texture uploads aren't flipped
|
|
159
|
+
glContext.pixelStorei(glContext.UNPACK_FLIP_Y_WEBGL, false);
|
|
160
|
+
|
|
154
161
|
// force it to set instanced mode
|
|
155
162
|
glSetInstancedMode(true);
|
|
156
163
|
}
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -258,7 +258,7 @@ const Ease =
|
|
|
258
258
|
* @param {number} x
|
|
259
259
|
* @returns {number}
|
|
260
260
|
* @memberof TweenSystem */
|
|
261
|
-
EXPO: (x) => 2 ** (10 * x - 10),
|
|
261
|
+
EXPO: (x) => x === 0 ? 0 : 2 ** (10 * x - 10),
|
|
262
262
|
|
|
263
263
|
/** Back ease-in: overshoots backward at the start before snapping forward.
|
|
264
264
|
* @param {number} x
|
|
@@ -271,6 +271,8 @@ const Ease =
|
|
|
271
271
|
* @returns {number}
|
|
272
272
|
* @memberof TweenSystem */
|
|
273
273
|
ELASTIC: (x) =>
|
|
274
|
+
x === 0 ? 0 :
|
|
275
|
+
x === 1 ? 1 :
|
|
274
276
|
-(2 ** (10 * x - 10)) * sin(((37 - 40 * x) * PI) / 6),
|
|
275
277
|
|
|
276
278
|
/** Spring-like ease-out: oscillates outward after passing the target.
|
|
@@ -431,29 +433,32 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
// Continuation that schedules the next loop iteration when one finishes.
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
436
|
+
// Reuses the same Tween object across iterations so the user's handle
|
|
437
|
+
// from `.loop()` keeps working — calling `.stop()` mid-loop now cancels
|
|
438
|
+
// the entire chain instead of just the current iteration.
|
|
439
|
+
function loopContinuation(tween)
|
|
437
440
|
{
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
442
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
443
|
+
tween.life = tween.duration;
|
|
444
|
+
tween.thenCallback = () => loopContinuation(tween);
|
|
445
|
+
tweenActive.push(tween);
|
|
446
|
+
// snap to start for the new iteration (matches Tween constructor behavior)
|
|
447
|
+
tween.callback(tween.interp(tween.duration));
|
|
445
448
|
}
|
|
446
449
|
|
|
447
|
-
// Continuation for pingPong:
|
|
448
|
-
function pingPongContinuation(
|
|
450
|
+
// Continuation for pingPong: swaps start and end on the same tween each iteration.
|
|
451
|
+
function pingPongContinuation(tween)
|
|
449
452
|
{
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
453
|
+
if (tween.loopRemaining !== Infinity && tween.loopRemaining <= 1) return;
|
|
454
|
+
if (tween.loopRemaining !== Infinity) tween.loopRemaining -= 1;
|
|
455
|
+
const tmp = tween.start;
|
|
456
|
+
tween.start = tween.end;
|
|
457
|
+
tween.end = tmp;
|
|
458
|
+
tween.life = tween.duration;
|
|
459
|
+
tween.thenCallback = () => pingPongContinuation(tween);
|
|
460
|
+
tweenActive.push(tween);
|
|
461
|
+
tween.callback(tween.interp(tween.duration));
|
|
457
462
|
}
|
|
458
463
|
|
|
459
464
|
/** Engine plugin hook: advance every active tween by the appropriate delta.
|
package/plugins/uiSystem.js
CHANGED
|
@@ -456,11 +456,17 @@ class UISystemPlugin
|
|
|
456
456
|
* @param {DragAndDropCallback} [onDragOver] - continuously when dragging over */
|
|
457
457
|
setupDragAndDrop(onDrop, onDragEnter, onDragLeave, onDragOver)
|
|
458
458
|
{
|
|
459
|
-
|
|
459
|
+
// remove any prior listeners so repeated setup calls don't stack
|
|
460
|
+
if (this._dragListeners)
|
|
461
|
+
for (const [type, listener] of this._dragListeners)
|
|
462
|
+
document.removeEventListener(type, listener);
|
|
463
|
+
this._dragListeners = [];
|
|
464
|
+
const setCallback = (callback, listenerType)=>
|
|
460
465
|
{
|
|
461
|
-
|
|
466
|
+
const listener = (e)=> { e.preventDefault(); callback && callback(e); };
|
|
462
467
|
document.addEventListener(listenerType, listener);
|
|
463
|
-
|
|
468
|
+
this._dragListeners.push([listenerType, listener]);
|
|
469
|
+
};
|
|
464
470
|
setCallback(onDrop, 'drop');
|
|
465
471
|
setCallback(onDragEnter, 'dragenter');
|
|
466
472
|
setCallback(onDragLeave, 'dragleave');
|
|
@@ -784,6 +790,15 @@ class UIObject
|
|
|
784
790
|
if (this.destroyed)
|
|
785
791
|
return;
|
|
786
792
|
|
|
793
|
+
// clear ui-system references that point at this object so events
|
|
794
|
+
// don't keep firing against a destroyed target (especially the
|
|
795
|
+
// keydown listener attached for keyInputObject)
|
|
796
|
+
if (uiSystem.activeObject === this) uiSystem.activeObject = undefined;
|
|
797
|
+
if (uiSystem.hoverObject === this) uiSystem.hoverObject = undefined;
|
|
798
|
+
if (uiSystem.lastHoverObject === this) uiSystem.lastHoverObject = undefined;
|
|
799
|
+
if (uiSystem.navigationObject === this) uiSystem.navigationObject = undefined;
|
|
800
|
+
if (uiSystem.keyInputObject === this) uiSystem.keyInputObject = undefined;
|
|
801
|
+
|
|
787
802
|
// disconnect from parent and destroy children
|
|
788
803
|
this.destroyed = 1;
|
|
789
804
|
this.parent?.removeChild(this);
|
|
@@ -792,6 +807,8 @@ class UIObject
|
|
|
792
807
|
child.parent = undefined;
|
|
793
808
|
child.destroy();
|
|
794
809
|
}
|
|
810
|
+
// clear references so destroyed children can be GC'd
|
|
811
|
+
this.children.length = 0;
|
|
795
812
|
}
|
|
796
813
|
|
|
797
814
|
/** Check if the mouse is overlapping this ui object
|
|
@@ -1381,6 +1398,7 @@ class UISlider extends UIObject
|
|
|
1381
1398
|
{
|
|
1382
1399
|
// toggle value between 0 and 1
|
|
1383
1400
|
this.value = this.value ? 0 : 1;
|
|
1401
|
+
this.onChange();
|
|
1384
1402
|
this.onRelease();
|
|
1385
1403
|
super.navigatePressed();
|
|
1386
1404
|
}
|
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.18.
|
|
35
|
+
const engineVersion = '1.18.15';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -161,12 +161,20 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
|
161
161
|
* ['tiles.png', 'tilesLevel.png'] // images to load
|
|
162
162
|
* );
|
|
163
163
|
* @memberof Engine */
|
|
164
|
-
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement
|
|
164
|
+
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
165
165
|
{
|
|
166
166
|
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
167
167
|
ASSERT(!mainContext, 'engine already initialized');
|
|
168
|
+
// runtime guard so release builds (where the assert is stripped) don't
|
|
169
|
+
// double-register listeners / double-add canvases on a second call
|
|
170
|
+
if (mainContext) return;
|
|
168
171
|
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
169
172
|
|
|
173
|
+
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
174
|
+
if (!document.body)
|
|
175
|
+
document.documentElement.appendChild(document.createElement('body'));
|
|
176
|
+
rootElement ||= document.body;
|
|
177
|
+
|
|
170
178
|
// allow passing in empty functions
|
|
171
179
|
gameInit ||= ()=>{};
|
|
172
180
|
gameUpdate ||= ()=>{};
|
|
@@ -192,6 +200,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
192
200
|
{
|
|
193
201
|
// update time keeping
|
|
194
202
|
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
203
|
+
// skip delta on the very first frame so timeReal doesn't jump
|
|
204
|
+
// by ~page-load-time when RAF starts handing real timestamps
|
|
205
|
+
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
195
206
|
frameTimeLastMS = frameTimeMS;
|
|
196
207
|
if (debug || debugWatermark)
|
|
197
208
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
@@ -422,7 +433,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
422
433
|
promises.push(loadTexture(0));
|
|
423
434
|
|
|
424
435
|
// load engine font image
|
|
425
|
-
promises.push(
|
|
436
|
+
promises.push(imageFontInit());
|
|
426
437
|
|
|
427
438
|
if (showSplashScreen)
|
|
428
439
|
{
|
package/src/engineAudio.js
CHANGED
|
@@ -103,7 +103,7 @@ class Sound
|
|
|
103
103
|
/** @property {SoundLoadCallback} - function to call when sound is loaded */
|
|
104
104
|
this.onloadCallback = onloadCallback;
|
|
105
105
|
|
|
106
|
-
if (
|
|
106
|
+
if (isArray(asset))
|
|
107
107
|
{
|
|
108
108
|
// generate zzfx sound — copy so we don't mutate the caller's array
|
|
109
109
|
const zzfxSound = asset.slice();
|
|
@@ -195,7 +195,7 @@ class Sound
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/** Get how long this sound is in seconds
|
|
198
|
-
* @return {number} - How long the sound is in seconds (
|
|
198
|
+
* @return {number} - How long the sound is in seconds (0 if loading)
|
|
199
199
|
*/
|
|
200
200
|
getDuration()
|
|
201
201
|
{ return this.sampleChannels?.[0]?.length / this.sampleRate || 0; }
|
|
@@ -352,10 +352,14 @@ class SoundInstance
|
|
|
352
352
|
{
|
|
353
353
|
if (fadeTime)
|
|
354
354
|
{
|
|
355
|
-
// ramp off gain
|
|
355
|
+
// ramp off gain from current volume (not 1, or low-volume
|
|
356
|
+
// instances would jump back up before fading);
|
|
357
|
+
// cancel any prior scheduling so stacked stop calls don't
|
|
358
|
+
// re-anchor partway through a previous fade
|
|
356
359
|
const startFade = audioContext.currentTime;
|
|
357
360
|
const endFade = startFade + fadeTime;
|
|
358
|
-
this.gainNode.gain.
|
|
361
|
+
this.gainNode.gain.cancelScheduledValues(startFade);
|
|
362
|
+
this.gainNode.gain.setValueAtTime(this.volume, startFade);
|
|
359
363
|
this.gainNode.gain.linearRampToValueAtTime(0, endFade);
|
|
360
364
|
this.source.stop(endFade);
|
|
361
365
|
}
|
|
@@ -403,13 +407,14 @@ class SoundInstance
|
|
|
403
407
|
*/
|
|
404
408
|
getCurrentTime()
|
|
405
409
|
{
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
410
|
+
if (!this.isPlaying()) return this.pausedTime;
|
|
411
|
+
const duration = this.getDuration();
|
|
412
|
+
// guard mod against 0 duration (rate=0 or sound not loaded)
|
|
413
|
+
return duration ? mod(audioContext.currentTime - this.startTime, duration) : 0;
|
|
409
414
|
}
|
|
410
415
|
|
|
411
416
|
/** Get the total duration of this sound
|
|
412
|
-
* @return {number} - Total duration in seconds
|
|
417
|
+
* @return {number} - Total duration in seconds (0 if loading)
|
|
413
418
|
*/
|
|
414
419
|
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
415
420
|
|
|
@@ -433,7 +438,7 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
433
438
|
{
|
|
434
439
|
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
435
440
|
if (!soundEnable || headlessMode) return;
|
|
436
|
-
if (
|
|
441
|
+
if (typeof speechSynthesis === 'undefined') return;
|
|
437
442
|
|
|
438
443
|
// common languages (not supported by all browsers)
|
|
439
444
|
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
@@ -451,7 +456,11 @@ function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
|
451
456
|
|
|
452
457
|
/** Stop all queued speech
|
|
453
458
|
* @memberof Audio */
|
|
454
|
-
function speakStop()
|
|
459
|
+
function speakStop()
|
|
460
|
+
{
|
|
461
|
+
if (typeof speechSynthesis !== 'undefined')
|
|
462
|
+
speechSynthesis.cancel();
|
|
463
|
+
}
|
|
455
464
|
|
|
456
465
|
/** Get frequency of a note on a musical scale
|
|
457
466
|
* @param {number} semitoneOffset - How many semitones away from the root note
|
|
@@ -513,9 +522,14 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
513
522
|
const pannerNode = new StereoPannerNode(audioContext, {'pan':clamp(pan, -1, 1)});
|
|
514
523
|
source.connect(pannerNode).connect(gainNode);
|
|
515
524
|
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
525
|
+
// disconnect nodes when the sound ends so the audio graph doesn't grow
|
|
526
|
+
// unbounded across many play() calls (source.stop() also fires 'ended')
|
|
527
|
+
source.addEventListener('ended', ()=>
|
|
528
|
+
{
|
|
529
|
+
gainNode.disconnect();
|
|
530
|
+
pannerNode.disconnect();
|
|
531
|
+
if (onended) onended(source);
|
|
532
|
+
});
|
|
519
533
|
|
|
520
534
|
// play and return sound
|
|
521
535
|
const startOffset = offset * rate;
|
package/src/engineBuild.mjs
CHANGED
|
@@ -196,7 +196,7 @@ function closureCompilerStep(filename)
|
|
|
196
196
|
fs.copyFileSync(filename, filenameTemp);
|
|
197
197
|
try
|
|
198
198
|
{
|
|
199
|
-
execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --
|
|
199
|
+
execSync(`npx google-closure-compiler --js=${filenameTemp} --js_output_file=${filename} --jscomp_off=*`);
|
|
200
200
|
fs.rmSync(filenameTemp);
|
|
201
201
|
}
|
|
202
202
|
catch (e) { handleError(e, 'Failed to run Closure Compiler step!'); }
|
package/src/engineDebug.js
CHANGED
|
@@ -83,12 +83,8 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
|
|
|
83
83
|
ASSERT(isNumber(time), 'time must be a number');
|
|
84
84
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
85
85
|
|
|
86
|
-
if (typeof size === 'number')
|
|
87
|
-
size = vec2(size); // allow passing in floats
|
|
88
86
|
if (isColor(color))
|
|
89
87
|
color = color.toString();
|
|
90
|
-
pos = pos.copy();
|
|
91
|
-
size = size.copy();
|
|
92
88
|
const timer = new Timer(time);
|
|
93
89
|
debugPrimitives.push({pos:pos.copy(), size:size.copy(), color, timer, angle, fill, screenSpace});
|
|
94
90
|
}
|
|
@@ -158,7 +154,7 @@ function debugPoint(pos, color, time, angle, screenSpace=false)
|
|
|
158
154
|
* @param {number} [time]
|
|
159
155
|
* @param {boolean} [screenSpace]
|
|
160
156
|
* @memberof Debug */
|
|
161
|
-
function debugLine(posA, posB, color, width=.1, time, screenSpace=false)
|
|
157
|
+
function debugLine(posA, posB, color, width=.1, time=0, screenSpace=false)
|
|
162
158
|
{
|
|
163
159
|
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
164
160
|
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
@@ -196,7 +192,7 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
196
192
|
debugRect(minPos.lerp(maxPos,.5), maxPos.subtract(minPos), color, time, 0, false, screenSpace);
|
|
197
193
|
}
|
|
198
194
|
|
|
199
|
-
/** Draw
|
|
195
|
+
/** Draw debug text in world space
|
|
200
196
|
* @param {string|number} text
|
|
201
197
|
* @param {Vector2} pos
|
|
202
198
|
* @param {number} [size]
|
|
@@ -527,7 +523,7 @@ function debugRender()
|
|
|
527
523
|
continue;
|
|
528
524
|
if (parseInt(i) < 3)
|
|
529
525
|
mousePressed += i + ' ' ;
|
|
530
|
-
else
|
|
526
|
+
else
|
|
531
527
|
keysPressed += i + ' ' ;
|
|
532
528
|
}
|
|
533
529
|
mousePressed && debugContext.fillText('Mouse: ' + mousePressed, x, y += h);
|