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/engineDraw.js
CHANGED
|
@@ -259,8 +259,9 @@ class TextureInfo
|
|
|
259
259
|
* @param {boolean} [useWebGL] - Should use WebGL if available?
|
|
260
260
|
* @param {boolean|Array<number>} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)? Or the WebGL
|
|
261
261
|
* modes across and down, like [gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT], as a glTF sampler gives them
|
|
262
|
+
* @param {boolean} [pixelated] - Hard edged or smooth for this texture alone, undefined follows tilesPixelated
|
|
262
263
|
*/
|
|
263
|
-
constructor(image, useWebGL=true, wrap=false)
|
|
264
|
+
constructor(image, useWebGL=true, wrap=false, pixelated)
|
|
264
265
|
{
|
|
265
266
|
/** @property {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas|ImageBitmap} - image source */
|
|
266
267
|
this.image = image;
|
|
@@ -275,6 +276,10 @@ class TextureInfo
|
|
|
275
276
|
* across and down
|
|
276
277
|
* @type {boolean|Array<number>} */
|
|
277
278
|
this.wrap = wrap;
|
|
279
|
+
/** @property {boolean|undefined} - Hard edged or smooth for this texture alone, a soft glow in a pixel art
|
|
280
|
+
* game or pixel art in a smooth one; undefined follows tilesPixelated
|
|
281
|
+
* @type {boolean|undefined} */
|
|
282
|
+
this.pixelated = pixelated;
|
|
278
283
|
useWebGL && this.createWebGLTexture();
|
|
279
284
|
}
|
|
280
285
|
|
|
@@ -305,15 +310,34 @@ class TextureInfo
|
|
|
305
310
|
this.wrap = wrap;
|
|
306
311
|
glSetTextureWrap(this.glTexture, wrap);
|
|
307
312
|
}
|
|
313
|
+
|
|
314
|
+
/** Make this texture hard edged or smooth on its own, whatever tilesPixelated says for the rest; it is made again
|
|
315
|
+
* with the new filtering
|
|
316
|
+
* @param {boolean} [pixelated] - undefined follows tilesPixelated again */
|
|
317
|
+
setPixelated(pixelated)
|
|
318
|
+
{
|
|
319
|
+
this.pixelated = pixelated;
|
|
320
|
+
if (!this.glTexture) return;
|
|
321
|
+
this.destroyWebGLTexture();
|
|
322
|
+
this.createWebGLTexture();
|
|
323
|
+
}
|
|
308
324
|
}
|
|
309
325
|
|
|
310
326
|
///////////////////////////////////////////////////////////////////////////////
|
|
327
|
+
/**
|
|
328
|
+
* @callback SpriteAnimationEndCallback - Called once when a SpriteAnimation's play ends
|
|
329
|
+
* @return {void}
|
|
330
|
+
* @memberof Draw
|
|
331
|
+
*/
|
|
332
|
+
|
|
311
333
|
/**
|
|
312
334
|
* SpriteAnimation - Steps a tile through its frames over time: looping, once, or there and back
|
|
313
335
|
* - Driven by the engine time like a Timer, so it pauses with the game and needs no update call
|
|
314
336
|
* - Read tileInfo each frame for the frame to draw, from an object's update or before a drawTile
|
|
315
337
|
* - loop, play and pingPong each start over from the first frame; stop holds the current one
|
|
338
|
+
* - play(onEnd) calls onEnd once the play ends, on the first read after it, since there is no update to call it
|
|
316
339
|
* - Frames follow each other along the row, as tileInfo.frame counts them
|
|
340
|
+
* - SpriteAnimator switches between a character's animations by name
|
|
317
341
|
* @example
|
|
318
342
|
* const walk = new SpriteAnimation(tile(0, 16), 4, .1); // four frames, a tenth of a second each
|
|
319
343
|
* const attack = new SpriteAnimation(tile(4, 16), 3, .05).play(); // once, then holds the last frame
|
|
@@ -345,6 +369,9 @@ class SpriteAnimation
|
|
|
345
369
|
/** @property {number|undefined} - The frame held by stop, undefined while running
|
|
346
370
|
* @type {number|undefined} */
|
|
347
371
|
this.heldFrame = undefined;
|
|
372
|
+
/** @property {SpriteAnimationEndCallback|undefined} - Called once when a play ends, on the first read after it
|
|
373
|
+
* @type {SpriteAnimationEndCallback|undefined} */
|
|
374
|
+
this.onEnd = undefined;
|
|
348
375
|
}
|
|
349
376
|
|
|
350
377
|
/** Start over from the first frame and repeat forever
|
|
@@ -352,8 +379,14 @@ class SpriteAnimation
|
|
|
352
379
|
loop() { return this.restart('loop'); }
|
|
353
380
|
|
|
354
381
|
/** Start over from the first frame, run through once and hold the last frame, last to first at a negative speed
|
|
382
|
+
* @param {function():void} [onEnd] - Called once when it ends, on the first read of it after that
|
|
355
383
|
* @return {SpriteAnimation} */
|
|
356
|
-
play()
|
|
384
|
+
play(onEnd)
|
|
385
|
+
{
|
|
386
|
+
this.restart('once');
|
|
387
|
+
this.onEnd = onEnd;
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
357
390
|
|
|
358
391
|
/** Start over from the first frame and run there and back forever
|
|
359
392
|
* @return {SpriteAnimation} */
|
|
@@ -363,7 +396,7 @@ class SpriteAnimation
|
|
|
363
396
|
* @return {SpriteAnimation} */
|
|
364
397
|
stop() { this.heldFrame = this.frame; return this; }
|
|
365
398
|
|
|
366
|
-
/** Start over from the first frame in a mode
|
|
399
|
+
/** Start over from the first frame in a mode, with no end callback
|
|
367
400
|
* @param {string} [mode] - 'loop', 'once' or 'pingPong', the current mode when left out
|
|
368
401
|
* @return {SpriteAnimation} */
|
|
369
402
|
restart(mode=this.mode)
|
|
@@ -371,6 +404,7 @@ class SpriteAnimation
|
|
|
371
404
|
this.mode = mode;
|
|
372
405
|
this.startTime = time;
|
|
373
406
|
this.heldFrame = undefined;
|
|
407
|
+
this.onEnd = undefined;
|
|
374
408
|
return this;
|
|
375
409
|
}
|
|
376
410
|
|
|
@@ -382,6 +416,7 @@ class SpriteAnimation
|
|
|
382
416
|
* @return {number} */
|
|
383
417
|
get frame()
|
|
384
418
|
{
|
|
419
|
+
this.isDone; // a read after a play ends calls onEnd, which may start it over
|
|
385
420
|
if (this.heldFrame !== undefined)
|
|
386
421
|
return this.heldFrame;
|
|
387
422
|
const n = this.frameCount, f = floor(this.elapsedFrames);
|
|
@@ -397,9 +432,81 @@ class SpriteAnimation
|
|
|
397
432
|
* @return {TileInfo} */
|
|
398
433
|
get tileInfo() { return this.firstTile.frame(this.frame); }
|
|
399
434
|
|
|
400
|
-
/** True once a play has shown its last frame for its time
|
|
435
|
+
/** True once a play has shown its last frame for its time, the first read after that calls onEnd
|
|
436
|
+
* @return {boolean} */
|
|
437
|
+
get isDone()
|
|
438
|
+
{
|
|
439
|
+
const done = this.mode == 'once' && this.heldFrame === undefined && abs(this.elapsedFrames) >= this.frameCount;
|
|
440
|
+
if (done && this.onEnd)
|
|
441
|
+
{
|
|
442
|
+
const onEnd = this.onEnd;
|
|
443
|
+
this.onEnd = undefined; // once, and it can start a new play with a callback of its own
|
|
444
|
+
onEnd();
|
|
445
|
+
}
|
|
446
|
+
return done;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
451
|
+
/**
|
|
452
|
+
* SpriteAnimator - A character's animations by name, like idle, walk and attack, and the one showing now
|
|
453
|
+
* - Each clip is a SpriteAnimation and keeps its own mode, loop, play or pingPong
|
|
454
|
+
* - set starts a clip over only when it changes or a play of it has ended, so it can be called every update
|
|
455
|
+
* - Read tileInfo each frame for the frame to draw, as with a SpriteAnimation
|
|
456
|
+
* @example
|
|
457
|
+
* const hero = new SpriteAnimator({
|
|
458
|
+
* idle: new SpriteAnimation(tile(0, 16), 2, .4),
|
|
459
|
+
* walk: new SpriteAnimation(tile(2, 16), 4, .1),
|
|
460
|
+
* attack: new SpriteAnimation(tile(6, 16), 3, .05).play(),
|
|
461
|
+
* });
|
|
462
|
+
* hero.set('attack', ()=> hero.set('idle')); // back to idle when the attack ends
|
|
463
|
+
* // in update: this.tileInfo = hero.tileInfo;
|
|
464
|
+
* @memberof Draw
|
|
465
|
+
*/
|
|
466
|
+
class SpriteAnimator
|
|
467
|
+
{
|
|
468
|
+
/** Create an animator from its clips, showing the first one
|
|
469
|
+
* @param {Object<string, SpriteAnimation>} clips - The clips by name */
|
|
470
|
+
constructor(clips)
|
|
471
|
+
{
|
|
472
|
+
const names = Object.keys(clips);
|
|
473
|
+
ASSERT(names.length > 0, 'an animator needs at least one clip');
|
|
474
|
+
/** @property {Object<string, SpriteAnimation>} - The clips by name */
|
|
475
|
+
this.clips = clips;
|
|
476
|
+
/** @property {string} - The name of the clip showing now */
|
|
477
|
+
this.name = names[0];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Show a clip, starting it over in its own mode unless it is showing and has not ended
|
|
481
|
+
* @param {string} name - The clip's name
|
|
482
|
+
* @param {function():void} [onEnd] - Called once when a play clip ends, as with SpriteAnimation.play
|
|
483
|
+
* @return {SpriteAnimator} */
|
|
484
|
+
set(name, onEnd)
|
|
485
|
+
{
|
|
486
|
+
ASSERT(this.clips[name], 'no clip named ' + name);
|
|
487
|
+
if (name == this.name && !this.clip.isDone)
|
|
488
|
+
return this;
|
|
489
|
+
this.name = name;
|
|
490
|
+
this.clip.restart();
|
|
491
|
+
this.clip.onEnd = onEnd;
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** The clip showing now
|
|
496
|
+
* @return {SpriteAnimation} */
|
|
497
|
+
get clip() { return this.clips[this.name]; }
|
|
498
|
+
|
|
499
|
+
/** The tile of the frame showing now
|
|
500
|
+
* @return {TileInfo} */
|
|
501
|
+
get tileInfo() { return this.clip.tileInfo; }
|
|
502
|
+
|
|
503
|
+
/** The frame of the clip showing now
|
|
504
|
+
* @return {number} */
|
|
505
|
+
get frame() { return this.clip.frame; }
|
|
506
|
+
|
|
507
|
+
/** True once the clip showing now is a play that has ended
|
|
401
508
|
* @return {boolean} */
|
|
402
|
-
get isDone() { return this.
|
|
509
|
+
get isDone() { return this.clip.isDone; }
|
|
403
510
|
}
|
|
404
511
|
|
|
405
512
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1224,7 +1331,7 @@ async function loadTexture(textureIndex, src)
|
|
|
1224
1331
|
const image = new Image;
|
|
1225
1332
|
if (src)
|
|
1226
1333
|
{
|
|
1227
|
-
await new Promise(resolve =>
|
|
1334
|
+
await engineAddLoad(new Promise(resolve => // startup waits for it
|
|
1228
1335
|
{
|
|
1229
1336
|
image.onload = resolve;
|
|
1230
1337
|
image.onerror = ()=>
|
|
@@ -1234,7 +1341,7 @@ async function loadTexture(textureIndex, src)
|
|
|
1234
1341
|
};
|
|
1235
1342
|
image.crossOrigin = 'anonymous';
|
|
1236
1343
|
image.src = src;
|
|
1237
|
-
});
|
|
1344
|
+
}));
|
|
1238
1345
|
}
|
|
1239
1346
|
|
|
1240
1347
|
return textureInfos[textureIndex] = new TextureInfo(image);
|
|
@@ -1757,6 +1864,33 @@ class ImageFont
|
|
|
1757
1864
|
}
|
|
1758
1865
|
}
|
|
1759
1866
|
|
|
1867
|
+
// how strong a light's glow is at a distance from its middle, 0 there to 1 at the edge: a bell, full in the middle
|
|
1868
|
+
// and nothing at the edge, fading faster the higher the falloff
|
|
1869
|
+
function engineGlowAlpha(r, falloff)
|
|
1870
|
+
{
|
|
1871
|
+
const k = 3.5 * falloff, edge = Math.exp(-k);
|
|
1872
|
+
return (Math.exp(-k * r * r) - edge) / (1 - edge);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// the soft round glow of the 2D and 3D lights, one texture for each falloff, rounded to a tenth so a changing falloff
|
|
1876
|
+
// makes only a few, each made once from a canvas; undefined headless or without a canvas
|
|
1877
|
+
const engineGlowTextures = new Map;
|
|
1878
|
+
function engineGlowTexture(falloff=1)
|
|
1879
|
+
{
|
|
1880
|
+
ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
|
|
1881
|
+
const key = max(round(falloff * 10), 1) / 10;
|
|
1882
|
+
let texture = engineGlowTextures.get(key);
|
|
1883
|
+
if (texture || !glContext || typeof OffscreenCanvas == 'undefined') return texture;
|
|
1884
|
+
const size = 64, context = createCanvasContext(size), steps = 16;
|
|
1885
|
+
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
1886
|
+
for (let i = 0; i <= steps; ++i)
|
|
1887
|
+
gradient.addColorStop(i / steps, 'rgba(255,255,255,' + engineGlowAlpha(i / steps, key).toFixed(4) + ')');
|
|
1888
|
+
context.fillStyle = gradient;
|
|
1889
|
+
context.fillRect(0, 0, size, size);
|
|
1890
|
+
engineGlowTextures.set(key, texture = new TextureInfo(context.canvas, true, false, false));
|
|
1891
|
+
return texture;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1760
1894
|
// load engine font, called automatically on startup
|
|
1761
1895
|
async function imageFontInit()
|
|
1762
1896
|
{
|
package/src/engineEditor.js
CHANGED
|
@@ -57,6 +57,14 @@ class LevelEditor
|
|
|
57
57
|
* section has Play from mouse, which starts play there, Escape at the mouse and Play at the view center
|
|
58
58
|
* @type {EditorPlayFromCallback|undefined} */
|
|
59
59
|
this.onPlayFrom = undefined;
|
|
60
|
+
/** @property {Array<number>|undefined} - The tiles the palette shows, in its order, for a sheet that also holds
|
|
61
|
+
* sprites and art that are not level tiles; undefined shows every tile of the sheet
|
|
62
|
+
* @type {Array<number>|undefined} */
|
|
63
|
+
this.paletteTiles = undefined;
|
|
64
|
+
/** @property {boolean|undefined} - Which level editor opens: true the 3D one, false the 2D one, undefined the
|
|
65
|
+
* 3D one when a level was loaded with level3DLoad and there is a Render3DPlugin
|
|
66
|
+
* @type {boolean|undefined} */
|
|
67
|
+
this.use3D = undefined;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/** True while the editor is open, the game is paused under it
|
|
@@ -253,7 +261,7 @@ function editorMapRestore(map)
|
|
|
253
261
|
if (!saved) return map;
|
|
254
262
|
// an autosave from before the map's shape was in the hash matches on its tiles and objects, and one from before
|
|
255
263
|
// objects were matches on its tiles; one of a resized map is resized to first
|
|
256
|
-
const sameFile = saved.hash === record.hash || !saved.width &&
|
|
264
|
+
const sameFile = saved.hash === record.hash || saved.savedHash === record.hash || !saved.width &&
|
|
257
265
|
(saved.hash === editorMapHash(data, objects) || !saved.objects && saved.hash === editorMapHash(data));
|
|
258
266
|
if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
|
|
259
267
|
editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
|
|
@@ -406,15 +414,17 @@ async function editorRememberedFile(record)
|
|
|
406
414
|
}
|
|
407
415
|
const editorFileKey = (record)=> (globalThis.location?.pathname ?? '') + ' ' + record.key;
|
|
408
416
|
|
|
409
|
-
// a map as a file Save wrote is the file from then on: Reset to file goes back to it,
|
|
410
|
-
//
|
|
411
|
-
//
|
|
417
|
+
// a map as a file Save wrote is the file from then on: Reset to file goes back to it, and a reload of it has nothing
|
|
418
|
+
// to apply; a download can not say it replaced the file, so it does not, and neither does a file of another name, a
|
|
419
|
+
// copy, since the game still loads the one it came from; the browser gives a picked file's name and not its folder,
|
|
420
|
+
// so a file of the same name may be a copy too, and the autosave stays, with the hash of the file the game loads and
|
|
421
|
+
// of the file written, until a reload shows which one the game loads
|
|
412
422
|
function editorSetBaseline(record, written)
|
|
413
423
|
{
|
|
414
424
|
if (record.synthetic) return; // a layer made in code has no file to load it from
|
|
415
425
|
const data = editorTileLayerData(written.layers);
|
|
416
426
|
const objects = editorObjectGroups(written.layers).map((group)=> group.objects ?? []);
|
|
417
|
-
Object.assign(record, {original: data, originalObjects: objects,
|
|
427
|
+
Object.assign(record, {original: data, originalObjects: objects, savedHash: editorMapHash(data, objects,
|
|
418
428
|
editorMapLayout(written)), originalSize: {width: written.width, height: written.height}});
|
|
419
429
|
record.pending || editorAutosave(record); // edits waiting to be applied keep their autosave
|
|
420
430
|
}
|
|
@@ -422,12 +432,21 @@ function editorSetBaseline(record, written)
|
|
|
422
432
|
// save a map as Tiled JSON: where the browser lets a page write files, Chrome and Edge, to a file picked once and
|
|
423
433
|
// written again on each Save after, even after a reload once the browser gives permission, or picked again with
|
|
424
434
|
// Save As; elsewhere as a download under the name of the file it came from; resolves to how it saved, undefined
|
|
425
|
-
// when the picker was closed
|
|
435
|
+
// when the picker was closed; saves of a map run one at a time in the order asked, so the file ends with the last
|
|
426
436
|
async function editorSave(record, pickAgain=false)
|
|
427
437
|
{
|
|
428
438
|
if (!record) return;
|
|
429
439
|
editorStrokeEnd();
|
|
430
|
-
const text = editorMapJSON(record),
|
|
440
|
+
const text = editorMapJSON(record); // the map as it is now, later edits wait for a later save
|
|
441
|
+
const saved = (async ()=> { await record.saving; return editorSaveText(record, text, pickAgain); })();
|
|
442
|
+
record.saving = saved.catch(()=> {});
|
|
443
|
+
return saved;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// write one save of a map, the one before it done
|
|
447
|
+
async function editorSaveText(record, text, pickAgain)
|
|
448
|
+
{
|
|
449
|
+
const picker = /** @type {any} */ (globalThis).showSaveFilePicker;
|
|
431
450
|
if (picker)
|
|
432
451
|
{
|
|
433
452
|
try
|
|
@@ -527,11 +546,12 @@ function editorAutosave(record)
|
|
|
527
546
|
while (kept.length > original.length && !kept.at(-1).length)
|
|
528
547
|
kept.pop(); // an Objects layer the editor made, empty again, is not an edit
|
|
529
548
|
const size = record.originalSize, sameSize = map.width === size.width && map.height === size.height;
|
|
530
|
-
|
|
549
|
+
// the map as it was loaded has nothing to keep; one a Save wrote is kept until a reload shows the file has it
|
|
550
|
+
if (sameSize && editorSameData(data, record.original) && editorSameData(kept, original) && !record.savedHash)
|
|
531
551
|
delete saves[record.key];
|
|
532
552
|
else
|
|
533
|
-
saves[record.key] = {hash: record.hash, width: map.width, height: map.height,
|
|
534
|
-
nextobjectid: map.nextobjectid};
|
|
553
|
+
saves[record.key] = {hash: record.hash, savedHash: record.savedHash, width: map.width, height: map.height,
|
|
554
|
+
layers: data, objects, nextobjectid: map.nextobjectid};
|
|
535
555
|
editorWriteSaves(saves);
|
|
536
556
|
}
|
|
537
557
|
|
|
@@ -1364,8 +1384,8 @@ function editorBrushTile()
|
|
|
1364
1384
|
// the brush takes the tile in a cell, an empty cell gives the Erase brush
|
|
1365
1385
|
function editorPick(layer, pos) { editorBrush = editorStampTile(editorGidAt(layer, pos) || 0); }
|
|
1366
1386
|
|
|
1367
|
-
// a palette slot into the brush: slot 0 is the Erase brush, slot n
|
|
1368
|
-
// an object layer slot n is the nth object type
|
|
1387
|
+
// a palette slot into the brush: slot 0 is the Erase brush, slot n the palette's nth tile, keeping a one tile
|
|
1388
|
+
// brush's turn; on an object layer slot n is the nth object type
|
|
1369
1389
|
function editorPalettePick(slot)
|
|
1370
1390
|
{
|
|
1371
1391
|
if (editorObjectLayer)
|
|
@@ -1376,7 +1396,8 @@ function editorPalettePick(slot)
|
|
|
1376
1396
|
return;
|
|
1377
1397
|
}
|
|
1378
1398
|
const t = editorBrushTile();
|
|
1379
|
-
|
|
1399
|
+
const tile = slot && (editorPaletteTiles(editorLayer)[slot - 1]?.tile ?? slot - 1); // every tile, in order
|
|
1400
|
+
editorBrush = editorStampTile(slot ? editorTileToGid(tile, t?.direction, t?.mirror) : 0);
|
|
1380
1401
|
}
|
|
1381
1402
|
|
|
1382
1403
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1795,11 +1816,18 @@ function editorPanelInit()
|
|
|
1795
1816
|
// the palette's cell size in pixels and how many to a row
|
|
1796
1817
|
const editorPaletteCell = 30, editorPaletteColumns = 8;
|
|
1797
1818
|
|
|
1798
|
-
// a layer's
|
|
1799
|
-
// ones at the end, a tile of one
|
|
1819
|
+
// the tiles a layer's palette shows, each its index and the tile info it draws with: the game's
|
|
1820
|
+
// levelEditor.paletteTiles, or every tile up to the image's edge without the blank ones at the end, a tile of one
|
|
1821
|
+
// flat color; found once for each layer and list
|
|
1800
1822
|
function editorPaletteTiles(layer)
|
|
1801
1823
|
{
|
|
1802
|
-
const live = layer?.live, image = live?.tileInfo?.textureInfo?.image;
|
|
1824
|
+
const live = layer?.live, image = live?.tileInfo?.textureInfo?.image, list = levelEditor.paletteTiles;
|
|
1825
|
+
if (list && live)
|
|
1826
|
+
{
|
|
1827
|
+
if (layer.palette?.list !== list)
|
|
1828
|
+
layer.palette = {list, tiles: list.map((tile)=> ({tile, tileInfo: editorTileInfo(live, tile)}))};
|
|
1829
|
+
return layer.palette.tiles;
|
|
1830
|
+
}
|
|
1803
1831
|
if (!image) return [];
|
|
1804
1832
|
if (layer.palette?.image === image) return layer.palette.tiles;
|
|
1805
1833
|
|
|
@@ -1808,7 +1836,7 @@ function editorPaletteTiles(layer)
|
|
|
1808
1836
|
{
|
|
1809
1837
|
const t = editorTileInfo(live, i);
|
|
1810
1838
|
if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
|
|
1811
|
-
tiles.push(t);
|
|
1839
|
+
tiles.push({tile: i, tileInfo: t});
|
|
1812
1840
|
}
|
|
1813
1841
|
try
|
|
1814
1842
|
{
|
|
@@ -1817,7 +1845,8 @@ function editorPaletteTiles(layer)
|
|
|
1817
1845
|
context.drawImage(image, 0, 0);
|
|
1818
1846
|
const blank = (t)=>
|
|
1819
1847
|
{
|
|
1820
|
-
const pixels = new Uint32Array(context.getImageData(
|
|
1848
|
+
const {pos, size} = t.tileInfo, pixels = new Uint32Array(context.getImageData(pos.x, pos.y, size.x,
|
|
1849
|
+
size.y).data.buffer);
|
|
1821
1850
|
return pixels.every((p)=> p === pixels[0]);
|
|
1822
1851
|
};
|
|
1823
1852
|
while (tiles.length > 1 && blank(tiles.at(-1)))
|
|
@@ -1877,15 +1906,16 @@ function editorPaletteDraw(canvas, layer)
|
|
|
1877
1906
|
context.moveTo(8, 8), context.lineTo(cell - 8, cell - 8);
|
|
1878
1907
|
context.moveTo(cell - 8, 8), context.lineTo(8, cell - 8);
|
|
1879
1908
|
context.stroke();
|
|
1880
|
-
tiles.forEach((
|
|
1909
|
+
tiles.forEach(({tileInfo: {pos, size}}, i)=>
|
|
1881
1910
|
{
|
|
1882
1911
|
const slot = i + 1, x = slot % columns * cell, y = (slot / columns | 0) * cell;
|
|
1883
|
-
context.drawImage(image,
|
|
1912
|
+
context.drawImage(image, pos.x, pos.y, size.x, size.y, x + 2, y + 2, cell - 4, cell - 4);
|
|
1884
1913
|
});
|
|
1885
1914
|
|
|
1886
|
-
// the brush's slot outlined, when it is one tile or Erase
|
|
1915
|
+
// the brush's slot outlined, when it is one tile the palette shows or Erase
|
|
1887
1916
|
const t = editorBrushTile(), erase = !t && editorBrush.width === 1 && editorBrush.height === 1 &&
|
|
1888
|
-
editorBrush.grids[0][0] === 0,
|
|
1917
|
+
editorBrush.grids[0][0] === 0, shown = t ? tiles.findIndex((p)=> p.tile === t.tile) : -1;
|
|
1918
|
+
const selected = erase ? 0 : shown >= 0 ? shown + 1 : -1;
|
|
1889
1919
|
if (selected < 0) return;
|
|
1890
1920
|
context.strokeStyle = '#4af';
|
|
1891
1921
|
context.strokeRect(selected % columns * cell + 1, (selected / columns | 0) * cell + 1, cell - 2, cell - 2);
|
package/src/engineExport.js
CHANGED
|
@@ -26,6 +26,7 @@ export
|
|
|
26
26
|
engineObjectsCallback,
|
|
27
27
|
engineObjectsRaycast,
|
|
28
28
|
engineAddPlugin,
|
|
29
|
+
engineAddLoad,
|
|
29
30
|
|
|
30
31
|
// Globals
|
|
31
32
|
debug,
|
|
@@ -70,8 +71,10 @@ export
|
|
|
70
71
|
canvasPixelRatio,
|
|
71
72
|
fontDefault,
|
|
72
73
|
showSplashScreen,
|
|
74
|
+
loadingScreen,
|
|
73
75
|
headlessMode,
|
|
74
76
|
engineManualStep,
|
|
77
|
+
engineVariableStep,
|
|
75
78
|
tileDefaultSize,
|
|
76
79
|
tileDefaultPadding,
|
|
77
80
|
tileDefaultBleed,
|
|
@@ -129,8 +132,10 @@ export
|
|
|
129
132
|
getCanvasPixelRatio,
|
|
130
133
|
setFontDefault,
|
|
131
134
|
setShowSplashScreen,
|
|
135
|
+
setLoadingScreen,
|
|
132
136
|
setHeadlessMode,
|
|
133
137
|
setEngineManualStep,
|
|
138
|
+
setEngineVariableStep,
|
|
134
139
|
setGLEnable,
|
|
135
140
|
setDrawScreenSpace,
|
|
136
141
|
setTileDefaultSize,
|
|
@@ -268,6 +273,7 @@ export
|
|
|
268
273
|
TextureInfo,
|
|
269
274
|
loadTexture,
|
|
270
275
|
SpriteAnimation,
|
|
276
|
+
SpriteAnimator,
|
|
271
277
|
Shader,
|
|
272
278
|
mainCanvas,
|
|
273
279
|
mainContext,
|
|
@@ -329,6 +335,8 @@ export
|
|
|
329
335
|
glFlush,
|
|
330
336
|
glCopyToContext,
|
|
331
337
|
glSetAntialias,
|
|
338
|
+
glMaxTextureSize,
|
|
339
|
+
glClampTextureSize,
|
|
332
340
|
glDraw,
|
|
333
341
|
glDrawUntextured,
|
|
334
342
|
glDrawPointsTransform,
|
package/src/engineInput.js
CHANGED
|
@@ -192,7 +192,7 @@ function keyIsDown(key, device=0)
|
|
|
192
192
|
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
193
193
|
ASSERT(typeof key !== 'string' || key.length > 1, "keys are codes like 'KeyW' or 'Space', not characters");
|
|
194
194
|
ASSERT(device > 0 || typeof key !== 'number' || key < 5, 'use code string for keyboard');
|
|
195
|
-
return !!(inputData[device]?.[key] & 1);
|
|
195
|
+
return !!(inputData[device]?.[key] & 1) && !inputCaptureHides(device);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/** Returns true if device key was pressed this frame
|
|
@@ -205,7 +205,7 @@ function keyWasPressed(key, device=0)
|
|
|
205
205
|
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
206
206
|
ASSERT(typeof key !== 'string' || key.length > 1, "keys are codes like 'KeyW' or 'Space', not characters");
|
|
207
207
|
ASSERT(device > 0 || typeof key !== 'number' || key < 5, 'use code string for keyboard');
|
|
208
|
-
return !!(inputData[device]?.[key] & 2);
|
|
208
|
+
return !!(inputData[device]?.[key] & 2) && !inputCaptureHides(device);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/** Returns true if device key was released this frame
|
|
@@ -218,7 +218,7 @@ function keyWasReleased(key, device=0)
|
|
|
218
218
|
ASSERT(isStringLike(key), 'key must be a number or string');
|
|
219
219
|
ASSERT(typeof key !== 'string' || key.length > 1, "keys are codes like 'KeyW' or 'Space', not characters");
|
|
220
220
|
ASSERT(device > 0 || typeof key !== 'number' || key < 5, 'use code string for keyboard');
|
|
221
|
-
return !!(inputData[device]?.[key] & 4);
|
|
221
|
+
return !!(inputData[device]?.[key] & 4) && !inputCaptureHides(device);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/** Returns input vector from arrow keys or WASD if enabled
|
|
@@ -421,7 +421,10 @@ function pointerLockRequest()
|
|
|
421
421
|
/** Request to unlock the pointer
|
|
422
422
|
* @memberof Input */
|
|
423
423
|
function pointerLockExit()
|
|
424
|
-
{
|
|
424
|
+
{
|
|
425
|
+
inputLockExit(); // debug builds: the free camera and the 3D editor tell this from Escape
|
|
426
|
+
document.exitPointerLock?.();
|
|
427
|
+
}
|
|
425
428
|
|
|
426
429
|
/** Check if pointer is locked (true if locked)
|
|
427
430
|
* @return {boolean}
|
|
@@ -738,6 +741,7 @@ function inputInit()
|
|
|
738
741
|
|
|
739
742
|
function inputUpdate()
|
|
740
743
|
{
|
|
744
|
+
inputCaptureMouse(); // debug builds: the free camera or the 3D editor may have the mouse
|
|
741
745
|
if (headlessMode) return;
|
|
742
746
|
|
|
743
747
|
// clear input when lost focus (prevent stuck keys)
|
package/src/engineLogo.js
CHANGED
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
///////////////////////////////////////////////////////////////////////////////
|
|
10
|
+
// the default loading screen: Loading over a bar across the middle, filled by the part of the loads done
|
|
11
|
+
function drawLoadingScreen(progress)
|
|
12
|
+
{
|
|
13
|
+
const x = mainContext, w = mainCanvasSize.x, h = mainCanvasSize.y;
|
|
14
|
+
const barWidth = min(w * .6, 400), barHeight = 12, left = (w - barWidth) / 2, top = h / 2;
|
|
15
|
+
x.save();
|
|
16
|
+
x.fillStyle = '#000';
|
|
17
|
+
x.fillRect(0, 0, w, h);
|
|
18
|
+
x.fillStyle = x.strokeStyle = '#fff';
|
|
19
|
+
x.font = '28px ' + fontDefault;
|
|
20
|
+
x.textAlign = 'center';
|
|
21
|
+
x.textBaseline = 'bottom';
|
|
22
|
+
x.fillText('Loading', w / 2, top - 16);
|
|
23
|
+
x.lineWidth = 2;
|
|
24
|
+
x.strokeRect(left, top, barWidth, barHeight);
|
|
25
|
+
x.fillRect(left, top, barWidth * clamp(progress), barHeight);
|
|
26
|
+
x.restore();
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
function drawEngineLogo(t)
|
|
11
30
|
{
|
|
12
31
|
const blackAndWhite = 0;
|
package/src/engineObject.js
CHANGED
|
@@ -188,9 +188,10 @@ class EngineObject
|
|
|
188
188
|
// bail if a collision callback destroyed us mid-frame
|
|
189
189
|
if (this.destroyed) return;
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
// limit max speed to prevent missing collisions, only for what collides: with solids, or with tiles while it
|
|
192
|
+
// has a mass, which tile collision needs; anything else moves as fast as it is told
|
|
193
|
+
if (this.clampSpeed && enablePhysicsSolver && (this.collideSolidObjects || this.collideLevel && this.mass))
|
|
192
194
|
{
|
|
193
|
-
// limit max speed to prevent missing collisions
|
|
194
195
|
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
195
196
|
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
196
197
|
}
|