littlejsengine 1.20.0 → 1.22.0
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/dist/littlejs.d.ts +1041 -81
- package/dist/littlejs.esm.js +6191 -379
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +6238 -476
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +3500 -387
- package/package.json +3 -2
- package/plugins/gltf.js +61 -9
- package/plugins/lightSystem.js +44 -1
- package/plugins/math3d.js +132 -30
- package/plugins/particleEffects.js +745 -0
- package/plugins/pluginExport.js +42 -0
- package/plugins/postProcess.js +246 -39
- package/plugins/render3d.js +850 -182
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +2576 -0
- package/plugins/render3dExtras.js +421 -15
- package/plugins/render3dLevel.js +352 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +59 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +18 -9
- package/src/engineBuild.mjs +4 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +141 -7
- package/src/engineEditor.js +52 -22
- package/src/engineExport.js +8 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
- package/src/engineWebGL.js +27 -10
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.
|
|
35
|
+
const engineVersion = '1.22.0';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -40,11 +40,12 @@ const engineVersion = '1.20.0';
|
|
|
40
40
|
* @memberof Engine */
|
|
41
41
|
const frameRate = 60;
|
|
42
42
|
|
|
43
|
-
/** How many seconds
|
|
43
|
+
/** How many seconds the update covers: 1/60 with the fixed time step, or with engineVariableStep the time of the
|
|
44
|
+
* display frame it runs on, times timeScale; while paused it keeps the last update's
|
|
44
45
|
* @type {number}
|
|
45
46
|
* @default 1/60
|
|
46
47
|
* @memberof Engine */
|
|
47
|
-
|
|
48
|
+
let timeDelta = 1/frameRate;
|
|
48
49
|
|
|
49
50
|
/** Array containing all engine objects
|
|
50
51
|
* @type {Array<EngineObject>}
|
|
@@ -92,6 +93,75 @@ function setPaused(isPaused=true) { paused = isPaused; }
|
|
|
92
93
|
|
|
93
94
|
// Engine internal variables
|
|
94
95
|
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
96
|
+
|
|
97
|
+
// delta smoothing, after Time Delta Smoothing by Frank Force (2013): a frame is on screen for whole display frames,
|
|
98
|
+
// so each delta is rounded to them and the rest is carried to the next, keeping the total real time; the frame
|
|
99
|
+
// length is estimated from recent frames and kept internal, since the browser does not say what it is
|
|
100
|
+
const frameDeltaHistory = [];
|
|
101
|
+
let frameIntervalMS = 1e3 / 60, frameDeltaCarryMS = 0, frameDeltaCarryAverageMS = 0, frameDeltaSmoothing = true;
|
|
102
|
+
function engineSmoothDelta(deltaMS)
|
|
103
|
+
{
|
|
104
|
+
if (!(deltaMS > 0)) return 0;
|
|
105
|
+
if (deltaMS < 250) // a gap from a hidden tab is not a display frame
|
|
106
|
+
{
|
|
107
|
+
frameDeltaHistory.push(deltaMS);
|
|
108
|
+
frameDeltaHistory.length > 64 && frameDeltaHistory.shift();
|
|
109
|
+
|
|
110
|
+
// the frames each delta held are counted in the last estimate or in the lower quartile of the last 16,
|
|
111
|
+
// whichever the deltas fit better: the quartile is one frame even when a busy game misses most of them, and
|
|
112
|
+
// it takes over when the window moves to a display with another refresh rate, or the estimate went wrong
|
|
113
|
+
const recent = frameDeltaHistory.slice(-16).sort((a, b)=> a - b);
|
|
114
|
+
const [lastMS, lastOff] = engineFrameFit(frameIntervalMS);
|
|
115
|
+
const [quartileMS, quartileOff] = engineFrameFit(recent[recent.length >> 2]);
|
|
116
|
+
frameIntervalMS = lastOff <= quartileOff ? lastMS : quartileMS;
|
|
117
|
+
|
|
118
|
+
// deltas far from whole frames mean the display has no fixed refresh, with a margin so it does not flip
|
|
119
|
+
frameDeltaSmoothing = min(lastOff, quartileOff) < (frameDeltaSmoothing ? .2 : .12);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!frameDeltaSmoothing)
|
|
123
|
+
{
|
|
124
|
+
// deltas that are not whole frames of one length, like a variable refresh display, are the frame times
|
|
125
|
+
// as they are, with any carry paid, and smoothing starts over once they fit again
|
|
126
|
+
const rawMS = deltaMS + frameDeltaCarryMS;
|
|
127
|
+
frameDeltaCarryMS = min(rawMS, 0);
|
|
128
|
+
frameDeltaCarryAverageMS = 0;
|
|
129
|
+
return max(rawMS, 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// whole frames, 0 for one that came early or 2 after one was missed, the carry stays within half a frame; a
|
|
133
|
+
// little of its slow average is paid each frame, so jitter can not hold it at half a frame and flip every frame
|
|
134
|
+
const pullMS = frameDeltaCarryAverageMS / 32;
|
|
135
|
+
frameDeltaCarryMS += deltaMS;
|
|
136
|
+
const frames = round((frameDeltaCarryMS - pullMS) / frameIntervalMS);
|
|
137
|
+
if (frames < 1)
|
|
138
|
+
return 0; // the carry keeps it
|
|
139
|
+
const smoothMS = frames * frameIntervalMS + pullMS;
|
|
140
|
+
frameDeltaCarryMS -= smoothMS;
|
|
141
|
+
frameDeltaCarryAverageMS += (frameDeltaCarryMS - frameDeltaCarryAverageMS) / 32;
|
|
142
|
+
return smoothMS;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// fit the delta history as whole frames of about unitMS: the frame length is the least squares slope of time over
|
|
146
|
+
// frames held, so jitter and whole ms timestamps average out, with how far the deltas are from whole frames
|
|
147
|
+
function engineFrameFit(unitMS)
|
|
148
|
+
{
|
|
149
|
+
let timeMS = 0, frames = 0, offMS = 0, sumF = 0, sumT = 0, sumFF = 0, sumFT = 0;
|
|
150
|
+
const count = frameDeltaHistory.length;
|
|
151
|
+
for (const d of frameDeltaHistory)
|
|
152
|
+
{
|
|
153
|
+
const held = max(1, round(d / unitMS));
|
|
154
|
+
timeMS += d;
|
|
155
|
+
frames += held;
|
|
156
|
+
offMS += abs(d - held * unitMS);
|
|
157
|
+
sumF += frames, sumT += timeMS, sumFF += frames * frames, sumFT += frames * timeMS;
|
|
158
|
+
}
|
|
159
|
+
const spread = count * sumFF - sumF * sumF;
|
|
160
|
+
return [spread ? (count * sumFT - sumF * sumT) / spread : unitMS, offMS / count / unitMS];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// where the fixed step's time counts from, moved when the variable step hands back so time goes on from there
|
|
164
|
+
let timeFixedStart = 0, frameFixedStart = 0;
|
|
95
165
|
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
96
166
|
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
97
167
|
let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
|
|
@@ -109,6 +179,9 @@ function engineObjectsCollidePairAdd(asker, other, resolve=false)
|
|
|
109
179
|
others.set(other, resolve);
|
|
110
180
|
}
|
|
111
181
|
let engineInitialized = false; // engineInit ran, with or without a canvas
|
|
182
|
+
// the loads startup waits for, each counted for the loading screen, and how many are done; undefined once the game
|
|
183
|
+
// loop starts
|
|
184
|
+
let engineLoads, engineLoadsDone = 0;
|
|
112
185
|
let engineObjectsUpdateCount = 0; // passes of engineObjectsUpdate so far, how a child knows it moved this pass
|
|
113
186
|
const engineChildStack = []; // the children being updated, taken off the live lists so one leaving does not skip the next
|
|
114
187
|
let showEngineVersion = true;
|
|
@@ -158,11 +231,68 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored, preRe
|
|
|
158
231
|
///////////////////////////////////////////////////////////////////////////////
|
|
159
232
|
// Main Engine Functions
|
|
160
233
|
|
|
234
|
+
/** Add something the game loads to what startup waits for: while engineInit and gameInit run, the game loop starts
|
|
235
|
+
* once it is done, and the loading screen counts it; images from loadTexture and sounds from files are added on
|
|
236
|
+
* their own, and a load that fails counts as done; after startup it does nothing
|
|
237
|
+
* @param {Promise<any>} promise
|
|
238
|
+
* @return {Promise<any>} - The same promise
|
|
239
|
+
* @example
|
|
240
|
+
* async function gameInit() { level = await engineAddLoad(fetchJSON('level.json')); }
|
|
241
|
+
* @memberof Engine */
|
|
242
|
+
function engineAddLoad(promise)
|
|
243
|
+
{
|
|
244
|
+
if (engineLoads)
|
|
245
|
+
{
|
|
246
|
+
engineLoads.push(promise);
|
|
247
|
+
const done = ()=> { ++engineLoadsDone; };
|
|
248
|
+
promise.then(done, done);
|
|
249
|
+
}
|
|
250
|
+
return promise;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// wait for every load, the ones added while waiting too, drawing the loading screen each frame meanwhile
|
|
254
|
+
async function engineWaitForLoads()
|
|
255
|
+
{
|
|
256
|
+
let waiting = true;
|
|
257
|
+
const start = performance.now();
|
|
258
|
+
const drawFrame = ()=>
|
|
259
|
+
{
|
|
260
|
+
if (!waiting) return;
|
|
261
|
+
engineLoadingScreenDraw((performance.now() - start) / 1e3);
|
|
262
|
+
setTimeout(drawFrame, 16);
|
|
263
|
+
};
|
|
264
|
+
headlessMode || drawFrame();
|
|
265
|
+
try
|
|
266
|
+
{
|
|
267
|
+
for (let count; count !== engineLoads.length;)
|
|
268
|
+
{
|
|
269
|
+
count = engineLoads.length;
|
|
270
|
+
await Promise.allSettled(engineLoads);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
finally { waiting = false; }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// one frame of the loading screen, once loading has taken half a second, so a fast load never shows it; input
|
|
277
|
+
// while it shows is dropped, as it is under the splash
|
|
278
|
+
function engineLoadingScreenDraw(elapsed)
|
|
279
|
+
{
|
|
280
|
+
if (headlessMode || !loadingScreen || elapsed < .5) return;
|
|
281
|
+
inputClear();
|
|
282
|
+
engineUpdateCanvas();
|
|
283
|
+
loadingScreen(engineLoadsDone / engineLoads.length);
|
|
284
|
+
}
|
|
285
|
+
|
|
161
286
|
/**
|
|
162
287
|
* @callback GameInitCallback - Called after the engine starts, can be async
|
|
163
288
|
* @return {void|Promise<void>}
|
|
164
289
|
* @memberof Engine
|
|
165
290
|
*/
|
|
291
|
+
/**
|
|
292
|
+
* @callback LoadingScreenCallback - Draws the loading screen on mainContext, each frame while the game loads
|
|
293
|
+
* @param {number} progress - The part of the loads done, 0 to 1
|
|
294
|
+
* @memberof Engine
|
|
295
|
+
*/
|
|
166
296
|
/**
|
|
167
297
|
* @callback GameCallback - Update or render function for the game
|
|
168
298
|
* @memberof Engine
|
|
@@ -197,6 +327,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
197
327
|
// double-register listeners / double-add canvases on a second call
|
|
198
328
|
if (engineInitialized) return;
|
|
199
329
|
engineInitialized = true;
|
|
330
|
+
engineLoads = [], engineLoadsDone = 0;
|
|
200
331
|
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
201
332
|
|
|
202
333
|
// allow passing in empty functions
|
|
@@ -236,6 +367,9 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
236
367
|
frameTimeLastMS = frameTimeMS;
|
|
237
368
|
if (debug || debugWatermark)
|
|
238
369
|
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
370
|
+
// the time the frame will be on screen, in whole display frames; engineStep's steps are exact already
|
|
371
|
+
if (!manualStepAtStart)
|
|
372
|
+
frameTimeDeltaMS = engineSmoothDelta(frameTimeDeltaMS);
|
|
239
373
|
audioUpdateVolume();
|
|
240
374
|
// the time keys work while the debug overlay is open, or always when debugKeysAlways is set
|
|
241
375
|
const debugKeys = debug && (debugOverlay || debugKeysAlways);
|
|
@@ -248,32 +382,61 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
248
382
|
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
249
383
|
const combinedScale = timeScale * debugScale;
|
|
250
384
|
frameTimeDeltaMS *= combinedScale;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const frozen = paused || !combinedScale;
|
|
254
|
-
frameTimeBufferMS += frozen ? frameTimeDeltaUnscaledMS : frameTimeDeltaMS;
|
|
255
|
-
frameTimeBufferMS = min(frameTimeBufferMS, 50 * (frozen ? 1 : max(1, combinedScale))); // clamp min framerate
|
|
256
|
-
|
|
257
|
-
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
258
|
-
let wasUpdated = false, deltaSmooth = 0;
|
|
259
|
-
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
385
|
+
let wasUpdated = false;
|
|
386
|
+
if (engineVariableStep)
|
|
260
387
|
{
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
388
|
+
// one update for the frame with timeDelta the time it covers, per-frame values are the game's to scale;
|
|
389
|
+
// engineStep's frames are exactly 1/60, its first too, which has no frame before it to take a delta from
|
|
390
|
+
if (frameTimeDeltaUnscaledMS > 0 || manualStepAtStart)
|
|
391
|
+
{
|
|
392
|
+
// frozen stands still as in the fixed step, and timeDelta keeps the last update's
|
|
393
|
+
const frozenTick = paused || !combinedScale;
|
|
394
|
+
if (!frozenTick)
|
|
395
|
+
{
|
|
396
|
+
timeDelta = manualStepAtStart ? combinedScale / frameRate :
|
|
397
|
+
min(frameTimeDeltaUnscaledMS, 50) * combinedScale / 1e3; // clamp min framerate
|
|
398
|
+
time += timeDelta;
|
|
399
|
+
++frame;
|
|
400
|
+
}
|
|
401
|
+
engineTick(frozenTick);
|
|
402
|
+
}
|
|
264
403
|
}
|
|
265
|
-
|
|
266
|
-
// update multiple frames if necessary in case of slow framerate
|
|
267
|
-
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
404
|
+
else
|
|
268
405
|
{
|
|
269
|
-
//
|
|
270
|
-
|
|
406
|
+
// paused or a time scale of 0 is frozen: it ticks on unscaled time, so the update rate stays fixed instead
|
|
407
|
+
// of following however fast the display refreshes, and gameUpdatePost and input still run to leave it
|
|
408
|
+
const frozen = paused || !combinedScale;
|
|
409
|
+
frameTimeBufferMS += frozen ? frameTimeDeltaUnscaledMS : frameTimeDeltaMS;
|
|
410
|
+
frameTimeBufferMS = min(frameTimeBufferMS, 50 * (frozen ? 1 : max(1, combinedScale))); // clamp min framerate
|
|
411
|
+
|
|
412
|
+
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
413
|
+
let deltaSmooth = 0;
|
|
414
|
+
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
415
|
+
{
|
|
416
|
+
// force at least one update each frame since it is waiting for refresh
|
|
417
|
+
deltaSmooth = frameTimeBufferMS;
|
|
418
|
+
frameTimeBufferMS = 0;
|
|
419
|
+
}
|
|
271
420
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
421
|
+
// update multiple frames if necessary in case of slow framerate
|
|
422
|
+
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
423
|
+
{
|
|
424
|
+
// read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
|
|
425
|
+
const frozenTick = paused || !(timeScale * debugScale);
|
|
275
426
|
|
|
276
|
-
|
|
427
|
+
// increment frame and update time, frozen does not advance time
|
|
428
|
+
if (!frozenTick)
|
|
429
|
+
time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
|
|
430
|
+
engineTick(frozenTick);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// add the time smoothing back in
|
|
434
|
+
frameTimeBufferMS += deltaSmooth;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// one tick of the loop: update game and objects, when frozen update everything except them
|
|
438
|
+
function engineTick(frozenTick)
|
|
439
|
+
{
|
|
277
440
|
wasUpdated = true;
|
|
278
441
|
engineUpdateCanvas();
|
|
279
442
|
inputUpdate();
|
|
@@ -300,9 +463,6 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
300
463
|
renderFrame();
|
|
301
464
|
}
|
|
302
465
|
|
|
303
|
-
// add the time smoothing back in
|
|
304
|
-
frameTimeBufferMS += deltaSmooth;
|
|
305
|
-
|
|
306
466
|
// manual step turned on by this frame's updates, set the buffer the loop and smoothing just moved again
|
|
307
467
|
if (engineManualStep && !manualStepAtStart)
|
|
308
468
|
frameTimeBufferMS = -.5e3 / frameRate;
|
|
@@ -361,7 +521,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
361
521
|
}
|
|
362
522
|
|
|
363
523
|
// skip setup if headless
|
|
364
|
-
if (headlessMode) return startEngine();
|
|
524
|
+
if (headlessMode) return startEngine([]);
|
|
365
525
|
|
|
366
526
|
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
367
527
|
if (!document.body)
|
|
@@ -439,14 +599,19 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
439
599
|
promises.push(splash);
|
|
440
600
|
}
|
|
441
601
|
|
|
442
|
-
//
|
|
443
|
-
await
|
|
444
|
-
return startEngine();
|
|
602
|
+
// the splash first, the images load under it, then the loading screen for the rest
|
|
603
|
+
showSplashScreen && await promises.at(-1);
|
|
604
|
+
return startEngine(promises);
|
|
445
605
|
|
|
446
|
-
|
|
606
|
+
// gameInit runs once the images are in, and the game loop starts once it and everything loaded while it ran are
|
|
607
|
+
// done, the loading screen showing in the meantime; an error in gameInit reaches the caller
|
|
608
|
+
async function startEngine(images)
|
|
447
609
|
{
|
|
448
|
-
|
|
449
|
-
|
|
610
|
+
const init = (async ()=> { await Promise.all(images); await gameInit(); })();
|
|
611
|
+
engineAddLoad(init);
|
|
612
|
+
await engineWaitForLoads();
|
|
613
|
+
engineLoads = undefined;
|
|
614
|
+
await init;
|
|
450
615
|
engineUpdateInternal = engineUpdate; // engineStep only runs once the game is set up
|
|
451
616
|
engineManualStep || engineUpdate();
|
|
452
617
|
}
|
package/src/engineAudio.js
CHANGED
|
@@ -220,9 +220,12 @@ class Sound
|
|
|
220
220
|
* @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
|
|
221
221
|
* @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
|
|
222
222
|
*/
|
|
223
|
-
constructor(asset, randomness, range
|
|
223
|
+
constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
|
|
224
224
|
{
|
|
225
225
|
if (!soundEnable || headlessMode) return;
|
|
226
|
+
const rangeIsDefault = range === undefined;
|
|
227
|
+
if (rangeIsDefault)
|
|
228
|
+
range = soundDefaultRange;
|
|
226
229
|
|
|
227
230
|
ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
|
|
228
231
|
ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
|
|
@@ -232,6 +235,9 @@ class Sound
|
|
|
232
235
|
|
|
233
236
|
/** @property {number} - World space max range of sound */
|
|
234
237
|
this.range = range;
|
|
238
|
+
/** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
|
|
239
|
+
* to render3D.soundDefaultRange instead, which is further */
|
|
240
|
+
this.rangeIsDefault = rangeIsDefault;
|
|
235
241
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
236
242
|
this.taper = taper;
|
|
237
243
|
/** @property {number} - How much to randomize frequency each time sound plays
|
|
@@ -281,8 +287,8 @@ class Sound
|
|
|
281
287
|
// load the audio file, a URL object as bundlers give works like its string;
|
|
282
288
|
// report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
|
|
283
289
|
const filename = asset + '';
|
|
284
|
-
this.loadSound(filename).catch(e=>
|
|
285
|
-
LOG('Sound load failed for', filename, '-', e.message));
|
|
290
|
+
engineAddLoad(this.loadSound(filename).catch(e=>
|
|
291
|
+
LOG('Sound load failed for', filename, '-', e.message))); // startup waits for it
|
|
286
292
|
}
|
|
287
293
|
}
|
|
288
294
|
|
|
@@ -867,7 +873,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
867
873
|
}
|
|
868
874
|
|
|
869
875
|
///////////////////////////////////////////////////////////////////////////////
|
|
870
|
-
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.
|
|
876
|
+
// ZzFXMicro - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
871
877
|
|
|
872
878
|
/** Generate and play a ZzFX sound
|
|
873
879
|
*
|
|
@@ -899,7 +905,7 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
|
|
|
899
905
|
* @param {number} [decay] - Decay time, how long to reach sustain after attack (seconds)
|
|
900
906
|
* @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
|
|
901
907
|
* @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
|
|
902
|
-
* @return {
|
|
908
|
+
* @return {Float32Array} - The audio samples
|
|
903
909
|
* @memberof Audio */
|
|
904
910
|
function zzfxG
|
|
905
911
|
(
|
|
@@ -937,7 +943,7 @@ function zzfxG
|
|
|
937
943
|
crush = 0, // bit crush offset
|
|
938
944
|
jump = 1, // pitch jump timer
|
|
939
945
|
length, // sample length
|
|
940
|
-
b
|
|
946
|
+
b, // sample buffer
|
|
941
947
|
t = 0, // sample time
|
|
942
948
|
i = 0, // sample index
|
|
943
949
|
s = 0, // sample value
|
|
@@ -964,9 +970,12 @@ function zzfxG
|
|
|
964
970
|
pitchJumpTime *= sampleRate;
|
|
965
971
|
repeatTime = repeatTime * sampleRate | 0;
|
|
966
972
|
|
|
973
|
+
// allocate the full sample buffer up front, much faster than growing an array
|
|
974
|
+
length = attack + decay + sustain + release + delay | 0;
|
|
975
|
+
b = new Float32Array(length > 0 ? length : 0);
|
|
976
|
+
|
|
967
977
|
// generate waveform
|
|
968
|
-
for (
|
|
969
|
-
i < length; b[i++] = s * volume) // sample
|
|
978
|
+
for (; i < length; b[i++] = s * volume) // sample
|
|
970
979
|
{
|
|
971
980
|
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
972
981
|
{
|
|
@@ -1002,7 +1011,7 @@ function zzfxG
|
|
|
1002
1011
|
|
|
1003
1012
|
f = (frequency += slide += deltaSlide) *// frequency
|
|
1004
1013
|
cos(modulation*modOffset++); // modulation
|
|
1005
|
-
t += f + f*noise*
|
|
1014
|
+
t += f + f*noise*(i*i*PI2%2-1); // noise
|
|
1006
1015
|
|
|
1007
1016
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
1008
1017
|
{
|
package/src/engineBuild.mjs
CHANGED
|
@@ -49,11 +49,14 @@ const enginePluginFiles =
|
|
|
49
49
|
`${PLUGIN_FOLDER}/drawUtilities.js`,
|
|
50
50
|
`${PLUGIN_FOLDER}/textureSheet.js`,
|
|
51
51
|
`${PLUGIN_FOLDER}/tweenSystem.js`,
|
|
52
|
+
`${PLUGIN_FOLDER}/sceneSystem.js`,
|
|
52
53
|
`${PLUGIN_FOLDER}/pathFinder.js`,
|
|
53
54
|
`${PLUGIN_FOLDER}/math3d.js`,
|
|
54
55
|
`${PLUGIN_FOLDER}/render3d.js`,
|
|
55
56
|
`${PLUGIN_FOLDER}/render3dExtras.js`,
|
|
56
57
|
`${PLUGIN_FOLDER}/render3dVoxels.js`,
|
|
58
|
+
`${PLUGIN_FOLDER}/render3dLevel.js`,
|
|
59
|
+
`${PLUGIN_FOLDER}/particleEffects.js`,
|
|
57
60
|
`${PLUGIN_FOLDER}/gltf.js`,
|
|
58
61
|
`${PLUGIN_FOLDER}/threejs.js`,
|
|
59
62
|
];
|
|
@@ -63,6 +66,7 @@ const engineDebugFiles =
|
|
|
63
66
|
`${PLUGIN_FOLDER}/tweakables.js`,
|
|
64
67
|
`${PLUGIN_FOLDER}/render3dDebug.js`,
|
|
65
68
|
`${SOURCE_FOLDER}/engineEditor.js`,
|
|
69
|
+
`${PLUGIN_FOLDER}/render3dEditor.js`,
|
|
66
70
|
];
|
|
67
71
|
const engineExtraFiles =
|
|
68
72
|
[
|
package/src/engineDebug.js
CHANGED
|
@@ -684,6 +684,14 @@ function debugRender()
|
|
|
684
684
|
debugContext.fillText('9: Tweakables', x, y += h);
|
|
685
685
|
debugContext.fillStyle = levelEditor.isOpen ? '#f00' : '#fff';
|
|
686
686
|
debugContext.fillText('0: Edit Level', x, y += h);
|
|
687
|
+
for (const line of debugOverlayKeys)
|
|
688
|
+
{
|
|
689
|
+
// a key a debug plugin added, when it has something to say
|
|
690
|
+
const key = line();
|
|
691
|
+
if (!key) continue;
|
|
692
|
+
debugContext.fillStyle = key.on ? '#f00' : '#fff';
|
|
693
|
+
debugContext.fillText(key.text, x, y += h);
|
|
694
|
+
}
|
|
687
695
|
debugContext.fillStyle = '#fff';
|
|
688
696
|
|
|
689
697
|
let keysPressed = '';
|
|
@@ -910,4 +918,50 @@ function debugProtectConstant(obj)
|
|
|
910
918
|
|
|
911
919
|
// freeze the object to prevent adding new properties
|
|
912
920
|
return Object.freeze(obj);
|
|
913
|
-
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
924
|
+
// Input capture: the free camera and the 3D level editor take the keyboard and mouse, so the game under them reads
|
|
925
|
+
// every key and mouse button as up, and no mouse movement or wheel; gamepads are left to the game
|
|
926
|
+
|
|
927
|
+
let inputCaptureOn = false; // something has taken the keyboard and mouse
|
|
928
|
+
let inputCaptureReading = false; // it is reading them now
|
|
929
|
+
let inputCaptureDeltaScreen, inputCaptureWheel = 0; // this update's mouse movement and wheel, for it alone
|
|
930
|
+
|
|
931
|
+
// the game let go of a captured mouse itself, with pointerLockExit, since the debug tools last looked: a capture
|
|
932
|
+
// that is lost without it is the browser's doing, which is how Chrome takes Escape
|
|
933
|
+
let inputLockLetGo = false;
|
|
934
|
+
function inputLockExit() { inputLockLetGo = true; }
|
|
935
|
+
|
|
936
|
+
// the keys debug plugins add to the overlay's list, each a function that gives {text, on} or nothing to show
|
|
937
|
+
const debugOverlayKeys = [];
|
|
938
|
+
|
|
939
|
+
// take the keyboard and mouse from the game, or hand them back
|
|
940
|
+
function inputCapture(on=true)
|
|
941
|
+
{
|
|
942
|
+
inputCaptureOn = !!on;
|
|
943
|
+
inputCaptureDeltaScreen = vec2();
|
|
944
|
+
inputCaptureWheel = 0;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// read the input as the one that took it, keyIsDown and the rest work as always inside read
|
|
948
|
+
function inputCaptureRead(read)
|
|
949
|
+
{
|
|
950
|
+
const was = inputCaptureReading;
|
|
951
|
+
inputCaptureReading = true;
|
|
952
|
+
try { return read(); }
|
|
953
|
+
finally { inputCaptureReading = was; }
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// if a read of the keyboard and mouse, device 0, is the game's while they are taken
|
|
957
|
+
function inputCaptureHides(device) { return inputCaptureOn && !inputCaptureReading && !device; }
|
|
958
|
+
|
|
959
|
+
// called first in inputUpdate: the mouse movement and wheel go to the one that took the input
|
|
960
|
+
function inputCaptureMouse()
|
|
961
|
+
{
|
|
962
|
+
if (!inputCaptureOn) return;
|
|
963
|
+
inputCaptureDeltaScreen = mouseDeltaScreen;
|
|
964
|
+
inputCaptureWheel = mouseWheel;
|
|
965
|
+
mouseDeltaScreen = vec2();
|
|
966
|
+
mouseWheel = 0;
|
|
967
|
+
}
|