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/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.20.0';
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 each frame lasts, engine uses a fixed time step
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
- const timeDelta = 1/frameRate;
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
- // paused or a time scale of 0 is frozen: it ticks on unscaled time, so the update rate stays fixed instead of
252
- // following however fast the display refreshes, and gameUpdatePost and input still run to leave it
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
- // force at least one update each frame since it is waiting for refresh
262
- deltaSmooth = frameTimeBufferMS;
263
- frameTimeBufferMS = 0;
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
- // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
270
- const frozenTick = paused || !(timeScale * debugScale);
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
- // increment frame and update time, frozen does not advance time
273
- if (!frozenTick)
274
- time = frame++ / frameRate;
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
- // update game and objects, when frozen update everything except them
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
- // wait for all the promises to finish
443
- await Promise.all(promises);
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
- async function startEngine()
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
- // wait for gameInit to load
449
- await gameInit();
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
  }
@@ -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=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
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.3.2 by Frank Force
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 {Array} - Array of audio samples
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 = [], // sample buffer
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 (length = attack + decay + sustain + release + delay | 0;
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*sin(i**5); // noise
1014
+ t += f + f*noise*(i*i*PI2%2-1); // noise
1006
1015
 
1007
1016
  if (jump && ++jump > pitchJumpTime) // pitch jump
1008
1017
  {
@@ -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
  [
@@ -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
+ }