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.
- package/CHANGELOG.md +234 -0
- package/COPYRIGHT.txt +38 -38
- package/EDITOR.md +359 -0
- package/FAQ.md +4 -9
- package/README.md +14 -8
- package/REFERENCE.md +2327 -0
- package/dist/littlejs.d.ts +149 -104
- package/dist/littlejs.esm.js +1652 -1332
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +1651 -1328
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1553 -1294
- package/package.json +5 -3
- package/plugins/gltf.js +29 -13
- package/plugins/pluginExport.js +0 -4
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +136 -31
- package/plugins/render3dEditor.js +51 -17
- package/plugins/render3dExtras.js +166 -70
- package/plugins/render3dLevel.js +52 -6
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/src/engine.js +1 -1
- package/src/engineAudio.js +9 -3
- package/src/engineBuild.mjs +17 -3
- package/src/engineDraw.js +11 -7
- package/src/engineEditor.js +52 -18
- package/src/engineExport.js +1 -0
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +5 -1
- package/src/engineTileLayer.js +119 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
- package/plugins/zzfxm.js +0 -160
package/src/engineAudio.js
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
-
//
|
|
890
|
+
// ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
885
891
|
|
|
886
892
|
/** Generate and play a ZzFX sound
|
|
887
893
|
*
|
package/src/engineBuild.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
1705
|
-
workReadCanvas.height
|
|
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
|
package/src/engineEditor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1525
|
-
from
|
|
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
|
}
|