littlejsengine 1.23.0 → 1.24.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.
@@ -7,7 +7,7 @@
7
7
  * - 2D spatial audio based on camera position with distance-based falloff
8
8
  * - Sound instance management (pause, resume, stop)
9
9
  * - Speech synthesis for text-to-speech
10
- * - Music playback with ZzFXM support
10
+ * - Music playback from audio files, looping by default
11
11
  * - Web Audio API integration with master gain control
12
12
  * - Sounds and the master bus can route through effects, see the audio effects plugin
13
13
  * @namespace Audio
@@ -222,7 +222,13 @@ class Sound
222
222
  */
223
223
  constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
224
224
  {
225
- if (!soundEnable || headlessMode) return;
225
+ if (!soundEnable || headlessMode)
226
+ {
227
+ // no sound is made: it counts as loaded, so a game that waits for its sounds goes on
228
+ this.loadedPercent = 1;
229
+ onloadCallback?.(this);
230
+ return;
231
+ }
226
232
  const rangeIsDefault = range === undefined;
227
233
  if (rangeIsDefault)
228
234
  range = soundDefaultRange;
@@ -881,7 +887,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
881
887
  }
882
888
 
883
889
  ///////////////////////////////////////////////////////////////////////////////
884
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
890
+ // ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
885
891
 
886
892
  /** Generate and play a ZzFX sound
887
893
  *
@@ -6,7 +6,7 @@
6
6
  * - Produces TypeScript definition file (.d.ts)
7
7
  * - Includes plugin files in builds
8
8
  * - Validates code and checks for errors
9
- * - Outputs to dist/ folder
9
+ * - Outputs to dist/ folder, which is left as it was when a step fails
10
10
  */
11
11
 
12
12
  import fs from 'node:fs';
@@ -19,7 +19,9 @@ const __dirname = dirname(__filename);
19
19
  const ROOT_DIR = join(__dirname, '..');
20
20
 
21
21
  const ENGINE_NAME = 'littlejs';
22
- const BUILD_FOLDER = join(ROOT_DIR, 'dist');
22
+ // built in a folder of its own and put in dist when all of it is there, so a step that fails leaves dist whole
23
+ const OUTPUT_FOLDER = join(ROOT_DIR, 'dist');
24
+ const BUILD_FOLDER = join(ROOT_DIR, 'dist.build');
23
25
  const SOURCE_FOLDER = join(ROOT_DIR, 'src');
24
26
  const PLUGIN_FOLDER = join(ROOT_DIR, 'plugins');
25
27
  const engineSourceFiles =
@@ -42,7 +44,6 @@ const enginePluginFiles =
42
44
  `${PLUGIN_FOLDER}/newgrounds.js`,
43
45
  `${PLUGIN_FOLDER}/postProcess.js`,
44
46
  `${PLUGIN_FOLDER}/lightSystem.js`,
45
- `${PLUGIN_FOLDER}/zzfxm.js`,
46
47
  `${PLUGIN_FOLDER}/audioEffects.js`,
47
48
  `${PLUGIN_FOLDER}/uiSystem.js`,
48
49
  `${PLUGIN_FOLDER}/box2d.js`,
@@ -104,6 +105,19 @@ catch (e) { handleError(e, 'Failed to create build folder!'); }
104
105
  // Build all versions
105
106
  await buildAll();
106
107
 
108
+ try
109
+ {
110
+ // every step is done: dist is given what was built, and loses what is no longer built
111
+ fs.mkdirSync(OUTPUT_FOLDER, { recursive: true });
112
+ const built = fs.readdirSync(BUILD_FOLDER);
113
+ for (const file of built)
114
+ fs.copyFileSync(join(BUILD_FOLDER, file), join(OUTPUT_FOLDER, file));
115
+ for (const file of fs.readdirSync(OUTPUT_FOLDER))
116
+ built.includes(file) || fs.rmSync(join(OUTPUT_FOLDER, file), { recursive: true, force: true });
117
+ fs.rmSync(BUILD_FOLDER, { recursive: true, force: true });
118
+ }
119
+ catch (e) { handleError(e, 'Failed to put the build in dist!'); }
120
+
107
121
  console.log(`Engine built in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds! ✨`);
108
122
 
109
123
  ///////////////////////////////////////////////////////////////////////////////
package/src/engineDraw.js CHANGED
@@ -943,7 +943,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
943
943
  // build regular polygon points
944
944
  const points = [];
945
945
  const sizeX = size.x/2, sizeY = size.y/2;
946
- for (let i=sides; i--;)
946
+ for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
947
947
  {
948
948
  const a = (i/sides)*PI*2;
949
949
  points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
@@ -1054,7 +1054,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
1054
1054
  if (!ring)
1055
1055
  {
1056
1056
  const points = [];
1057
- for (let i=sides; i--;)
1057
+ for (let i=sides; i-- > 0;)
1058
1058
  {
1059
1059
  const a = (i/sides)*PI*2;
1060
1060
  points.push(vec2(sin(a), cos(a)));
@@ -1154,7 +1154,7 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
1154
1154
  const startA = (offset%sides)/sides*PI*2;
1155
1155
  const points = [rim(startA)];
1156
1156
  const colors = [outerInt];
1157
- for (let i=sides; i--;)
1157
+ for (let i=sides; i-- > 0;)
1158
1158
  {
1159
1159
  const a = ((i+offset)%sides)/sides*PI*2;
1160
1160
  points.push(pos);
@@ -1577,7 +1577,8 @@ function isOnScreen(pos, size=0)
1577
1577
  function setAdditiveBlendMode(additive=true)
1578
1578
  {
1579
1579
  glAdditive = additive;
1580
- drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1580
+ if (drawContext) // none headless
1581
+ drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1581
1582
  }
1582
1583
 
1583
1584
  /** Set the Shader that 2D draws use from now on, none for the engine's own
@@ -1700,9 +1701,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
1700
1701
  }
1701
1702
  else
1702
1703
  {
1703
- // copy to offscreen canvas
1704
- workReadCanvas.width = sWidth;
1705
- workReadCanvas.height = sHeight;
1704
+ // copy to offscreen canvas, sized again only when the size changes: setting a size remakes the canvas even
1705
+ // to the same one, and a game's tinted tiles are mostly of one size
1706
+ if (workReadCanvas.width !== sWidth || workReadCanvas.height !== sHeight)
1707
+ workReadCanvas.width = sWidth, workReadCanvas.height = sHeight;
1708
+ else
1709
+ workReadContext.clearRect(0, 0, sWidth, sHeight);
1706
1710
  workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
1707
1711
 
1708
1712
  // tint image using offscreen work context
@@ -181,10 +181,10 @@ class LevelEditor
181
181
  onPanel(element) {}
182
182
 
183
183
  /** Called by Save with the text of the file and its name; return true when the game kept it itself, and the
184
- * editor writes no file, to set or override
184
+ * editor writes no file, to set or override; it may be async
185
185
  * @param {string} text
186
186
  * @param {string} fileName
187
- * @return {boolean|void} */
187
+ * @return {boolean|void|Promise<boolean|void>} */
188
188
  onSave(text, fileName) { return false; }
189
189
 
190
190
  /** True while the editor is open, the game is paused under it
@@ -551,7 +551,7 @@ function editorMapRestore(map)
551
551
  if (!map.layers || data.some((layer)=> !Array.isArray(layer))) return map;
552
552
  const objects = editorObjectGroups(map.layers).map((group)=> group.objects ?? []);
553
553
  const url = editorFetchedURLs.get(map), hash = editorMapHash(data, objects, editorMapLayout(map));
554
- const fileName = url?.split(/[?#]/)[0].split('/').pop() || 'level.json';
554
+ const fileName = editorFileName(url, 'level.json');
555
555
  const record = {map, url, fileName, key: editorMapKey(map, url, hash), hash,
556
556
  original: data.map((layer)=> [...layer]), originalObjects: editorObjectsCopy(objects),
557
557
  originalSize: {width: map.width, height: map.height}, layers: []};
@@ -582,6 +582,8 @@ function editorMapRestore(map)
582
582
  editorRestoreObjects(map, saved);
583
583
  console.warn(`LittleJS editor: brought back unsaved edits to ${record.fileName}, ` +
584
584
  'Save in the editor (Esc then 0) writes them to the file');
585
+ saved.stale && console.warn(`LittleJS editor: they are older than the last edits to ${record.fileName}, ` +
586
+ 'which storage had no room for');
585
587
  }
586
588
  return map;
587
589
  }
@@ -783,6 +785,7 @@ async function editorSaveText(record, text, pickAgain)
783
785
  if (error?.name === 'AbortError') return; // the picker was closed, nothing saved
784
786
  record.fileHandle = undefined; // a file it could not write, a download instead, and not kept
785
787
  editorFileStore.set(editorFileKey(record), undefined);
788
+ console.warn(`LittleJS editor: ${record.fileName} could not be written, downloaded instead`, error);
786
789
  }
787
790
  }
788
791
  saveText(text, record.fileName, 'application/json');
@@ -799,15 +802,35 @@ const editorSaves = ()=> readSaveData(editorSaveName(), {});
799
802
  // if the last autosave did not fit in storage, the panel says so
800
803
  let editorSaveFailed = false;
801
804
 
802
- // write the autosaves, noting whether storage took them
803
- function editorWriteSaves(saves)
805
+ // write the autosaves, noting whether storage took them; when it did not, the autosave it still has of this map
806
+ // is an older one, and is marked so, for the reload that brings it back to say so
807
+ function editorWriteSaves(saves, key, name=editorSaveName())
804
808
  {
805
809
  try
806
810
  {
807
- localStorage.setItem(editorSaveName(), JSON.stringify(saves));
808
- editorSaveFailed = false;
811
+ localStorage.setItem(name, JSON.stringify(saves));
812
+ return editorSaveFailed = false;
809
813
  }
810
814
  catch { editorSaveFailed = true; }
815
+ try
816
+ {
817
+ const kept = readSaveData(name, {});
818
+ if (!kept[key] || kept[key].stale) return true;
819
+ kept[key].stale = true;
820
+ localStorage.setItem(name, JSON.stringify(kept));
821
+ }
822
+ catch {} // no room for the mark either
823
+ return true;
824
+ }
825
+
826
+ // the name of the file a level was fetched from, as it is on disk: without its folder and query, and with what
827
+ // the url spells with a percent as it is written, so it is the name a picked file has
828
+ function editorFileName(url, fallback)
829
+ {
830
+ const name = url?.split(/[?#]/)[0].split('/').pop();
831
+ if (!name) return fallback;
832
+ try { return decodeURIComponent(name); }
833
+ catch { return name; }
811
834
  }
812
835
 
813
836
  // a map's name for its autosave, the file it was fetched from without a query, or its size, layer names and data as loaded; a map with
@@ -863,12 +886,12 @@ function editorAutosave(record)
863
886
  else
864
887
  saves[record.key] = {hash: record.hash, savedHash: record.savedHash, width: map.width, height: map.height,
865
888
  layers: data, objects, nextobjectid: map.nextobjectid};
866
- editorWriteSaves(saves);
889
+ editorWriteSaves(saves, record.key);
867
890
  }
868
891
 
869
892
  // paint every cell of a map's layers from a list of tile data, the tile layers of the map in order, and set its
870
893
  // object layers' objects from a list of them; data of another size resizes the map, which needs the Restart hook
871
- function editorPaintData(record, data, objects, width=record.map.width, height=record.map.height)
894
+ function editorPaintData(record, data, objects, width=record.map.width, height=record.map.height, keepUnknown=false)
872
895
  {
873
896
  editorStrokeEnd();
874
897
  if (width !== record.map.width || height !== record.map.height)
@@ -888,7 +911,9 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
888
911
  if (gids?.length === width * height)
889
912
  gids.forEach((gid, i)=> editorPaint(layer, vec2(i % width, height - 1 - (i / width | 0)), gid));
890
913
  }
891
- editorObjectLayers(record).forEach((layer, i)=> objects &&
914
+ // an autosave from before the file had an object layer leaves that layer as the file has it; Reset to file
915
+ // empties a layer the file did not have
916
+ editorObjectLayers(record).forEach((layer, i)=> objects && !(keepUnknown && i >= objects.length) &&
892
917
  editorChangeObjects(layer, (list)=> list.splice(0, list.length, ...editorObjectsCopy(objects[i] ?? []))));
893
918
  });
894
919
  editorStroke ? editorStrokeEnd() : editorAutosave(record);
@@ -908,7 +933,7 @@ function editorApplyPending(record)
908
933
  return false;
909
934
  }
910
935
  record.pending = record.pendingUnfit = undefined;
911
- editorPaintData(record, saved.layers, saved.objects, saved.width, saved.height);
936
+ editorPaintData(record, saved.layers, saved.objects, saved.width, saved.height, true);
912
937
  return true;
913
938
  }
914
939
 
@@ -1013,8 +1038,7 @@ function editorClearSelections()
1013
1038
  function editorMapChanged(record, before)
1014
1039
  {
1015
1040
  const stroke = [{resize: record, before, after: editorMapSnapshot(record)}];
1016
- editorUndoList.push(stroke);
1017
- editorRedoList.length = 0;
1041
+ editorUndoPush(stroke);
1018
1042
  editorChanged(stroke);
1019
1043
  }
1020
1044
 
@@ -1091,12 +1115,20 @@ function editorPaint(layer, pos, gid)
1091
1115
  (editorStroke ||= []).push({layer, pos: pos.copy(), before, after: gid});
1092
1116
  }
1093
1117
 
1118
+ // an edit goes on the undo list, which keeps the last 100, as the 3D editor's does: a fill of a large layer is
1119
+ // an entry for every cell, and a session of them would hold millions
1120
+ function editorUndoPush(stroke)
1121
+ {
1122
+ editorUndoList.push(stroke);
1123
+ editorUndoList.length > 100 && editorUndoList.shift();
1124
+ editorRedoList.length = 0;
1125
+ }
1126
+
1094
1127
  // the stroke is done, it can be undone as one
1095
1128
  function editorStrokeEnd()
1096
1129
  {
1097
1130
  if (!editorStroke) return;
1098
- editorUndoList.push(editorStroke);
1099
- editorRedoList.length = 0;
1131
+ editorUndoPush(editorStroke);
1100
1132
  editorChanged(editorStroke);
1101
1133
  editorStroke = undefined;
1102
1134
  }
@@ -1506,7 +1538,7 @@ function editorUpdateObjects(space)
1506
1538
  if (!editorObjectSelection.has(hit.id))
1507
1539
  editorObjectSelection = new Set([hit.id]);
1508
1540
  editorObjectDrag = {start: snap(mouse), from: new Map(editorSelectedObjects().map((object)=>
1509
- [object.id, editorObjectPos(record, object)]))};
1541
+ [object.id, {x: object.x, y: object.y}]))};
1510
1542
  }
1511
1543
  else if (editorObjectSelection.size)
1512
1544
  editorObjectSelection.clear();
@@ -1521,8 +1553,10 @@ function editorUpdateObjects(space)
1521
1553
  {
1522
1554
  for (const object of list)
1523
1555
  {
1524
- const from = drag.from.get(object.id);
1525
- from && editorObjectSetPos(record, object, from.add(delta));
1556
+ // from the numbers it had, in the map's own units: through cells and back they pick up a hair
1557
+ const from = drag.from.get(object.id), {tilewidth=1, tileheight=1} = record.map;
1558
+ if (from)
1559
+ object.x = from.x + delta.x * tilewidth, object.y = from.y - delta.y * tileheight;
1526
1560
  }
1527
1561
  });
1528
1562
  }
@@ -419,6 +419,7 @@ export
419
419
  tileCollisionTest,
420
420
  tileCollisionRaycast,
421
421
  tileLayersLoad,
422
+ tileLayersFromLDtk,
422
423
  objectLayersAddType,
423
424
  objectLayersLoad,
424
425
  TileLayerData,