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/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() { return this.restart('once'); }
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.mode == 'once' && this.heldFrame === undefined && abs(this.elapsedFrames) >= this.frameCount; }
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
  {
@@ -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, the autosave keeps only the
410
- // edits since, and a reload of it has nothing to apply; a download can not say it replaced the file, so it does not,
411
- // and neither does a file of another name, a copy, since the game still loads the one it came from
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, hash: editorMapHash(data, 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), picker = /** @type {any} */ (globalThis).showSaveFilePicker;
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
- if (sameSize && editorSameData(data, record.original) && editorSameData(kept, original))
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, layers: data, objects,
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 tile n - 1, keeping a one tile brush's turn; on
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
- editorBrush = editorStampTile(slot ? editorTileToGid(slot - 1, t?.direction, t?.mirror) : 0);
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 tiles in order, as the tile infos it draws them with, up to the image's edge and without the blank
1799
- // ones at the end, a tile of one flat color; found once for each layer
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(t.pos.x, t.pos.y, t.size.x, t.size.y).data.buffer);
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((t, i)=>
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, t.pos.x, t.pos.y, t.size.x, t.size.y, x + 2, y + 2, cell - 4, cell - 4);
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, selected = erase ? 0 : t ? t.tile + 1 : -1;
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);
@@ -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,
@@ -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
- { document.exitPointerLock?.(); }
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;
@@ -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
- if (this.clampSpeed)
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
  }