littlejsengine 1.20.0 → 1.21.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 +408 -26
- package/dist/littlejs.esm.js +3360 -161
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3344 -161
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1448 -141
- package/package.json +1 -1
- package/plugins/gltf.js +54 -8
- package/plugins/math3d.js +90 -10
- package/plugins/pluginExport.js +10 -0
- package/plugins/postProcess.js +41 -12
- package/plugins/render3d.js +424 -55
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +199 -5
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +3 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +97 -6
- package/src/engineEditor.js +41 -20
- package/src/engineExport.js +6 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
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.21.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
|
@@ -281,8 +281,8 @@ class Sound
|
|
|
281
281
|
// load the audio file, a URL object as bundlers give works like its string;
|
|
282
282
|
// report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
|
|
283
283
|
const filename = asset + '';
|
|
284
|
-
this.loadSound(filename).catch(e=>
|
|
285
|
-
LOG('Sound load failed for', filename, '-', e.message));
|
|
284
|
+
engineAddLoad(this.loadSound(filename).catch(e=>
|
|
285
|
+
LOG('Sound load failed for', filename, '-', e.message))); // startup waits for it
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
package/src/engineBuild.mjs
CHANGED
|
@@ -49,11 +49,13 @@ 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`,
|
|
57
59
|
`${PLUGIN_FOLDER}/gltf.js`,
|
|
58
60
|
`${PLUGIN_FOLDER}/threejs.js`,
|
|
59
61
|
];
|
|
@@ -63,6 +65,7 @@ const engineDebugFiles =
|
|
|
63
65
|
`${PLUGIN_FOLDER}/tweakables.js`,
|
|
64
66
|
`${PLUGIN_FOLDER}/render3dDebug.js`,
|
|
65
67
|
`${SOURCE_FOLDER}/engineEditor.js`,
|
|
68
|
+
`${PLUGIN_FOLDER}/render3dEditor.js`,
|
|
66
69
|
];
|
|
67
70
|
const engineExtraFiles =
|
|
68
71
|
[
|
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
|
+
}
|
package/src/engineDraw.js
CHANGED
|
@@ -308,12 +308,20 @@ class TextureInfo
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
///////////////////////////////////////////////////////////////////////////////
|
|
311
|
+
/**
|
|
312
|
+
* @callback SpriteAnimationEndCallback - Called once when a SpriteAnimation's play ends
|
|
313
|
+
* @return {void}
|
|
314
|
+
* @memberof Draw
|
|
315
|
+
*/
|
|
316
|
+
|
|
311
317
|
/**
|
|
312
318
|
* SpriteAnimation - Steps a tile through its frames over time: looping, once, or there and back
|
|
313
319
|
* - Driven by the engine time like a Timer, so it pauses with the game and needs no update call
|
|
314
320
|
* - Read tileInfo each frame for the frame to draw, from an object's update or before a drawTile
|
|
315
321
|
* - loop, play and pingPong each start over from the first frame; stop holds the current one
|
|
322
|
+
* - play(onEnd) calls onEnd once the play ends, on the first read after it, since there is no update to call it
|
|
316
323
|
* - Frames follow each other along the row, as tileInfo.frame counts them
|
|
324
|
+
* - SpriteAnimator switches between a character's animations by name
|
|
317
325
|
* @example
|
|
318
326
|
* const walk = new SpriteAnimation(tile(0, 16), 4, .1); // four frames, a tenth of a second each
|
|
319
327
|
* const attack = new SpriteAnimation(tile(4, 16), 3, .05).play(); // once, then holds the last frame
|
|
@@ -345,6 +353,9 @@ class SpriteAnimation
|
|
|
345
353
|
/** @property {number|undefined} - The frame held by stop, undefined while running
|
|
346
354
|
* @type {number|undefined} */
|
|
347
355
|
this.heldFrame = undefined;
|
|
356
|
+
/** @property {SpriteAnimationEndCallback|undefined} - Called once when a play ends, on the first read after it
|
|
357
|
+
* @type {SpriteAnimationEndCallback|undefined} */
|
|
358
|
+
this.onEnd = undefined;
|
|
348
359
|
}
|
|
349
360
|
|
|
350
361
|
/** Start over from the first frame and repeat forever
|
|
@@ -352,8 +363,14 @@ class SpriteAnimation
|
|
|
352
363
|
loop() { return this.restart('loop'); }
|
|
353
364
|
|
|
354
365
|
/** Start over from the first frame, run through once and hold the last frame, last to first at a negative speed
|
|
366
|
+
* @param {function():void} [onEnd] - Called once when it ends, on the first read of it after that
|
|
355
367
|
* @return {SpriteAnimation} */
|
|
356
|
-
play()
|
|
368
|
+
play(onEnd)
|
|
369
|
+
{
|
|
370
|
+
this.restart('once');
|
|
371
|
+
this.onEnd = onEnd;
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
357
374
|
|
|
358
375
|
/** Start over from the first frame and run there and back forever
|
|
359
376
|
* @return {SpriteAnimation} */
|
|
@@ -363,7 +380,7 @@ class SpriteAnimation
|
|
|
363
380
|
* @return {SpriteAnimation} */
|
|
364
381
|
stop() { this.heldFrame = this.frame; return this; }
|
|
365
382
|
|
|
366
|
-
/** Start over from the first frame in a mode
|
|
383
|
+
/** Start over from the first frame in a mode, with no end callback
|
|
367
384
|
* @param {string} [mode] - 'loop', 'once' or 'pingPong', the current mode when left out
|
|
368
385
|
* @return {SpriteAnimation} */
|
|
369
386
|
restart(mode=this.mode)
|
|
@@ -371,6 +388,7 @@ class SpriteAnimation
|
|
|
371
388
|
this.mode = mode;
|
|
372
389
|
this.startTime = time;
|
|
373
390
|
this.heldFrame = undefined;
|
|
391
|
+
this.onEnd = undefined;
|
|
374
392
|
return this;
|
|
375
393
|
}
|
|
376
394
|
|
|
@@ -382,6 +400,7 @@ class SpriteAnimation
|
|
|
382
400
|
* @return {number} */
|
|
383
401
|
get frame()
|
|
384
402
|
{
|
|
403
|
+
this.isDone; // a read after a play ends calls onEnd, which may start it over
|
|
385
404
|
if (this.heldFrame !== undefined)
|
|
386
405
|
return this.heldFrame;
|
|
387
406
|
const n = this.frameCount, f = floor(this.elapsedFrames);
|
|
@@ -397,9 +416,81 @@ class SpriteAnimation
|
|
|
397
416
|
* @return {TileInfo} */
|
|
398
417
|
get tileInfo() { return this.firstTile.frame(this.frame); }
|
|
399
418
|
|
|
400
|
-
/** True once a play has shown its last frame for its time
|
|
419
|
+
/** True once a play has shown its last frame for its time, the first read after that calls onEnd
|
|
401
420
|
* @return {boolean} */
|
|
402
|
-
get isDone()
|
|
421
|
+
get isDone()
|
|
422
|
+
{
|
|
423
|
+
const done = this.mode == 'once' && this.heldFrame === undefined && abs(this.elapsedFrames) >= this.frameCount;
|
|
424
|
+
if (done && this.onEnd)
|
|
425
|
+
{
|
|
426
|
+
const onEnd = this.onEnd;
|
|
427
|
+
this.onEnd = undefined; // once, and it can start a new play with a callback of its own
|
|
428
|
+
onEnd();
|
|
429
|
+
}
|
|
430
|
+
return done;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
435
|
+
/**
|
|
436
|
+
* SpriteAnimator - A character's animations by name, like idle, walk and attack, and the one showing now
|
|
437
|
+
* - Each clip is a SpriteAnimation and keeps its own mode, loop, play or pingPong
|
|
438
|
+
* - set starts a clip over only when it changes or a play of it has ended, so it can be called every update
|
|
439
|
+
* - Read tileInfo each frame for the frame to draw, as with a SpriteAnimation
|
|
440
|
+
* @example
|
|
441
|
+
* const hero = new SpriteAnimator({
|
|
442
|
+
* idle: new SpriteAnimation(tile(0, 16), 2, .4),
|
|
443
|
+
* walk: new SpriteAnimation(tile(2, 16), 4, .1),
|
|
444
|
+
* attack: new SpriteAnimation(tile(6, 16), 3, .05).play(),
|
|
445
|
+
* });
|
|
446
|
+
* hero.set('attack', ()=> hero.set('idle')); // back to idle when the attack ends
|
|
447
|
+
* // in update: this.tileInfo = hero.tileInfo;
|
|
448
|
+
* @memberof Draw
|
|
449
|
+
*/
|
|
450
|
+
class SpriteAnimator
|
|
451
|
+
{
|
|
452
|
+
/** Create an animator from its clips, showing the first one
|
|
453
|
+
* @param {Object<string, SpriteAnimation>} clips - The clips by name */
|
|
454
|
+
constructor(clips)
|
|
455
|
+
{
|
|
456
|
+
const names = Object.keys(clips);
|
|
457
|
+
ASSERT(names.length > 0, 'an animator needs at least one clip');
|
|
458
|
+
/** @property {Object<string, SpriteAnimation>} - The clips by name */
|
|
459
|
+
this.clips = clips;
|
|
460
|
+
/** @property {string} - The name of the clip showing now */
|
|
461
|
+
this.name = names[0];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Show a clip, starting it over in its own mode unless it is showing and has not ended
|
|
465
|
+
* @param {string} name - The clip's name
|
|
466
|
+
* @param {function():void} [onEnd] - Called once when a play clip ends, as with SpriteAnimation.play
|
|
467
|
+
* @return {SpriteAnimator} */
|
|
468
|
+
set(name, onEnd)
|
|
469
|
+
{
|
|
470
|
+
ASSERT(this.clips[name], 'no clip named ' + name);
|
|
471
|
+
if (name == this.name && !this.clip.isDone)
|
|
472
|
+
return this;
|
|
473
|
+
this.name = name;
|
|
474
|
+
this.clip.restart();
|
|
475
|
+
this.clip.onEnd = onEnd;
|
|
476
|
+
return this;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** The clip showing now
|
|
480
|
+
* @return {SpriteAnimation} */
|
|
481
|
+
get clip() { return this.clips[this.name]; }
|
|
482
|
+
|
|
483
|
+
/** The tile of the frame showing now
|
|
484
|
+
* @return {TileInfo} */
|
|
485
|
+
get tileInfo() { return this.clip.tileInfo; }
|
|
486
|
+
|
|
487
|
+
/** The frame of the clip showing now
|
|
488
|
+
* @return {number} */
|
|
489
|
+
get frame() { return this.clip.frame; }
|
|
490
|
+
|
|
491
|
+
/** True once the clip showing now is a play that has ended
|
|
492
|
+
* @return {boolean} */
|
|
493
|
+
get isDone() { return this.clip.isDone; }
|
|
403
494
|
}
|
|
404
495
|
|
|
405
496
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1224,7 +1315,7 @@ async function loadTexture(textureIndex, src)
|
|
|
1224
1315
|
const image = new Image;
|
|
1225
1316
|
if (src)
|
|
1226
1317
|
{
|
|
1227
|
-
await new Promise(resolve =>
|
|
1318
|
+
await engineAddLoad(new Promise(resolve => // startup waits for it
|
|
1228
1319
|
{
|
|
1229
1320
|
image.onload = resolve;
|
|
1230
1321
|
image.onerror = ()=>
|
|
@@ -1234,7 +1325,7 @@ async function loadTexture(textureIndex, src)
|
|
|
1234
1325
|
};
|
|
1235
1326
|
image.crossOrigin = 'anonymous';
|
|
1236
1327
|
image.src = src;
|
|
1237
|
-
});
|
|
1328
|
+
}));
|
|
1238
1329
|
}
|
|
1239
1330
|
|
|
1240
1331
|
return textureInfos[textureIndex] = new TextureInfo(image);
|