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.
@@ -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
  }
@@ -527,11 +537,12 @@ function editorAutosave(record)
527
537
  while (kept.length > original.length && !kept.at(-1).length)
528
538
  kept.pop(); // an Objects layer the editor made, empty again, is not an edit
529
539
  const size = record.originalSize, sameSize = map.width === size.width && map.height === size.height;
530
- if (sameSize && editorSameData(data, record.original) && editorSameData(kept, original))
540
+ // the map as it was loaded has nothing to keep; one a Save wrote is kept until a reload shows the file has it
541
+ if (sameSize && editorSameData(data, record.original) && editorSameData(kept, original) && !record.savedHash)
531
542
  delete saves[record.key];
532
543
  else
533
- saves[record.key] = {hash: record.hash, width: map.width, height: map.height, layers: data, objects,
534
- nextobjectid: map.nextobjectid};
544
+ saves[record.key] = {hash: record.hash, savedHash: record.savedHash, width: map.width, height: map.height,
545
+ layers: data, objects, nextobjectid: map.nextobjectid};
535
546
  editorWriteSaves(saves);
536
547
  }
537
548
 
@@ -1364,8 +1375,8 @@ function editorBrushTile()
1364
1375
  // the brush takes the tile in a cell, an empty cell gives the Erase brush
1365
1376
  function editorPick(layer, pos) { editorBrush = editorStampTile(editorGidAt(layer, pos) || 0); }
1366
1377
 
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
1378
+ // a palette slot into the brush: slot 0 is the Erase brush, slot n the palette's nth tile, keeping a one tile
1379
+ // brush's turn; on an object layer slot n is the nth object type
1369
1380
  function editorPalettePick(slot)
1370
1381
  {
1371
1382
  if (editorObjectLayer)
@@ -1376,7 +1387,8 @@ function editorPalettePick(slot)
1376
1387
  return;
1377
1388
  }
1378
1389
  const t = editorBrushTile();
1379
- editorBrush = editorStampTile(slot ? editorTileToGid(slot - 1, t?.direction, t?.mirror) : 0);
1390
+ const tile = slot && (editorPaletteTiles(editorLayer)[slot - 1]?.tile ?? slot - 1); // every tile, in order
1391
+ editorBrush = editorStampTile(slot ? editorTileToGid(tile, t?.direction, t?.mirror) : 0);
1380
1392
  }
1381
1393
 
1382
1394
  ///////////////////////////////////////////////////////////////////////////////
@@ -1795,11 +1807,18 @@ function editorPanelInit()
1795
1807
  // the palette's cell size in pixels and how many to a row
1796
1808
  const editorPaletteCell = 30, editorPaletteColumns = 8;
1797
1809
 
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
1810
+ // the tiles a layer's palette shows, each its index and the tile info it draws with: the game's
1811
+ // levelEditor.paletteTiles, or every tile up to the image's edge without the blank ones at the end, a tile of one
1812
+ // flat color; found once for each layer and list
1800
1813
  function editorPaletteTiles(layer)
1801
1814
  {
1802
- const live = layer?.live, image = live?.tileInfo?.textureInfo?.image;
1815
+ const live = layer?.live, image = live?.tileInfo?.textureInfo?.image, list = levelEditor.paletteTiles;
1816
+ if (list && live)
1817
+ {
1818
+ if (layer.palette?.list !== list)
1819
+ layer.palette = {list, tiles: list.map((tile)=> ({tile, tileInfo: editorTileInfo(live, tile)}))};
1820
+ return layer.palette.tiles;
1821
+ }
1803
1822
  if (!image) return [];
1804
1823
  if (layer.palette?.image === image) return layer.palette.tiles;
1805
1824
 
@@ -1808,7 +1827,7 @@ function editorPaletteTiles(layer)
1808
1827
  {
1809
1828
  const t = editorTileInfo(live, i);
1810
1829
  if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
1811
- tiles.push(t);
1830
+ tiles.push({tile: i, tileInfo: t});
1812
1831
  }
1813
1832
  try
1814
1833
  {
@@ -1817,7 +1836,8 @@ function editorPaletteTiles(layer)
1817
1836
  context.drawImage(image, 0, 0);
1818
1837
  const blank = (t)=>
1819
1838
  {
1820
- const pixels = new Uint32Array(context.getImageData(t.pos.x, t.pos.y, t.size.x, t.size.y).data.buffer);
1839
+ const {pos, size} = t.tileInfo, pixels = new Uint32Array(context.getImageData(pos.x, pos.y, size.x,
1840
+ size.y).data.buffer);
1821
1841
  return pixels.every((p)=> p === pixels[0]);
1822
1842
  };
1823
1843
  while (tiles.length > 1 && blank(tiles.at(-1)))
@@ -1877,15 +1897,16 @@ function editorPaletteDraw(canvas, layer)
1877
1897
  context.moveTo(8, 8), context.lineTo(cell - 8, cell - 8);
1878
1898
  context.moveTo(cell - 8, 8), context.lineTo(8, cell - 8);
1879
1899
  context.stroke();
1880
- tiles.forEach((t, i)=>
1900
+ tiles.forEach(({tileInfo: {pos, size}}, i)=>
1881
1901
  {
1882
1902
  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);
1903
+ context.drawImage(image, pos.x, pos.y, size.x, size.y, x + 2, y + 2, cell - 4, cell - 4);
1884
1904
  });
1885
1905
 
1886
- // the brush's slot outlined, when it is one tile or Erase
1906
+ // the brush's slot outlined, when it is one tile the palette shows or Erase
1887
1907
  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;
1908
+ editorBrush.grids[0][0] === 0, shown = t ? tiles.findIndex((p)=> p.tile === t.tile) : -1;
1909
+ const selected = erase ? 0 : shown >= 0 ? shown + 1 : -1;
1889
1910
  if (selected < 0) return;
1890
1911
  context.strokeStyle = '#4af';
1891
1912
  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,
@@ -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
  }
@@ -52,16 +52,28 @@ function tweak(){}
52
52
  function tweakButton(){}
53
53
  function tweakDivider(){}
54
54
  function tweakEngineDefaults(){}
55
- const levelEditor = {isOpen: false, open(){}, close(){}, onTile: undefined, onRestart: undefined, onPlayFrom: undefined};
55
+ const levelEditor = {isOpen: false, open(){}, close(){}, onTile: undefined, onRestart: undefined, onPlayFrom: undefined,
56
+ paletteTiles: undefined, use3D: undefined};
56
57
  function editorMapRestore(map){ return map; }
57
58
  function editorMapLoaded(){}
58
59
  function editorJSONFetched(){}
59
60
  function editorPreRender(){}
60
61
  function editorObjectMade(){}
61
62
 
63
+ // the input capture of the free camera and the 3D editor
64
+ function inputCaptureHides(){ return false; }
65
+ function inputCaptureMouse(){}
66
+ function inputLockExit(){}
67
+
62
68
  // the 3D debug draws are debug only too
63
69
  function debugBox3D(){}
64
70
  function debugSphere3D(){}
65
71
  function debugLine3D(){}
66
72
  function debugPoint3D(){}
67
73
  function render3DRenderDebug(){}
74
+
75
+ // the free camera and the 3D level editor are debug only
76
+ function editor3DCameraBegin(){}
77
+ function editor3DCameraEnd(){}
78
+ function editor3DLevelLoaded(){}
79
+ function editor3DObjectMade(){}
@@ -122,6 +122,13 @@ let fontDefault = 'arial';
122
122
  * @memberof Settings */
123
123
  let showSplashScreen = false;
124
124
 
125
+ /** The loading screen, drawn after the splash while the images, gameInit and what they load are loading, once that
126
+ * takes more than half a second: a function given the part done, 0 to 1, or undefined for none, which leaves the
127
+ * screen blank; the default says Loading over a bar
128
+ * @type {LoadingScreenCallback|undefined}
129
+ * @memberof Settings */
130
+ let loadingScreen = drawLoadingScreen;
131
+
125
132
  /** Disables all rendering, audio, and input for servers, must be set before engineInit
126
133
  * @type {boolean}
127
134
  * @default
@@ -135,6 +142,15 @@ let headlessMode = false;
135
142
  * @memberof Settings */
136
143
  let engineManualStep = false;
137
144
 
145
+ /** Run one update per display frame with timeDelta the time it covers, in place of fixed updates at frameRate
146
+ * - Per-frame values are not scaled: velocity, gravity, damping and particle speeds act once per update, so a
147
+ * game that turns this on scales its own movement by timeDelta
148
+ * - Values in seconds follow real time as they are: time, timers, particle emit rate and life, animation, Box2D
149
+ * @type {boolean}
150
+ * @default
151
+ * @memberof Settings */
152
+ let engineVariableStep = false;
153
+
138
154
  ///////////////////////////////////////////////////////////////////////////////
139
155
  // WebGL settings
140
156
 
@@ -530,6 +546,11 @@ function setFontDefault(font) { fontDefault = font; }
530
546
  * @memberof Settings */
531
547
  function setShowSplashScreen(show) { showSplashScreen = show; }
532
548
 
549
+ /** Set the loading screen, drawn after the splash while the game loads, once that takes more than half a second
550
+ * @param {LoadingScreenCallback} [callback] - Draws on mainContext given the part done, 0 to 1; undefined for none
551
+ * @memberof Settings */
552
+ function setLoadingScreen(callback) { loadingScreen = callback; }
553
+
533
554
  /** Set to disable rendering, audio, and input for servers, must be set before engineInit
534
555
  * @param {boolean} headless
535
556
  * @memberof Settings */
@@ -555,6 +576,23 @@ function setEngineManualStep(enable=true)
555
576
  }
556
577
  }
557
578
 
579
+ /** Set if the engine runs one update per display frame with timeDelta the time it covers
580
+ * Can be set before engineInit or while running; turned off it goes back to fixed updates at frameRate
581
+ * @param {boolean} [enable]
582
+ * @memberof Settings */
583
+ function setEngineVariableStep(enable=true)
584
+ {
585
+ if (engineVariableStep && !enable)
586
+ {
587
+ // fixed time goes on one fixed step after the last update, if one ran, and timeDelta is the fixed step again
588
+ timeFixedStart = frame ? time + 1 / frameRate : time;
589
+ frameFixedStart = frame;
590
+ timeDelta = 1 / frameRate;
591
+ frameTimeBufferMS = engineManualStep ? -.5e3 / frameRate : 0;
592
+ }
593
+ engineVariableStep = enable;
594
+ }
595
+
558
596
  /** Set whether the draw and debug functions draw in screen space when a call leaves screenSpace out
559
597
  * @param {boolean} screenSpace
560
598
  * @memberof Settings */