littlejsengine 1.23.1 → 1.24.1
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 +261 -0
- package/COPYRIGHT.txt +0 -27
- package/FAQ.md +4 -9
- package/README.md +4 -3
- package/REFERENCE.md +34 -18
- package/dist/littlejs.d.ts +160 -105
- package/dist/littlejs.esm.js +490 -277
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +489 -273
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +463 -264
- package/package.json +2 -1
- package/plugins/box2d.js +1 -1
- package/plugins/pluginExport.js +0 -4
- package/plugins/render3d.js +93 -17
- package/plugins/render3dEditor.js +9 -3
- package/plugins/render3dExtras.js +169 -70
- package/plugins/render3dLevel.js +24 -4
- package/plugins/render3dVoxels.js +15 -8
- package/src/engine.js +1 -1
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +0 -1
- package/src/engineEditor.js +17 -6
- package/src/engineExport.js +1 -0
- package/src/engineSettings.js +5 -1
- package/src/engineTileLayer.js +154 -1
- package/plugins/zzfxm.js +0 -162
package/plugins/render3dLevel.js
CHANGED
|
@@ -84,8 +84,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
84
84
|
* are added before its objects are made, one the game added itself keeps its place
|
|
85
85
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
86
86
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
87
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and
|
|
88
|
-
* leaves out stays as the game set it, and a level with no block changes
|
|
87
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
|
|
88
|
+
* sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
|
|
89
|
+
* nothing
|
|
89
90
|
* @param {Object} level - The level, the level editor edits this same object
|
|
90
91
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
91
92
|
* @memberof Level3D */
|
|
@@ -310,8 +311,25 @@ function level3DSceneApply(scene)
|
|
|
310
311
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
311
312
|
if (typeof scene.shadows === 'boolean')
|
|
312
313
|
r.shadows = scene.shadows;
|
|
314
|
+
if (typeof scene.lensFlare === 'boolean')
|
|
315
|
+
level3DSceneFlare(scene.lensFlare);
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
// the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
|
|
319
|
+
// earlier level's, and off takes away the one a scene made, never the game's
|
|
320
|
+
let level3DSunFlare;
|
|
321
|
+
function level3DSceneFlare(on)
|
|
322
|
+
{
|
|
323
|
+
if (!on)
|
|
324
|
+
{
|
|
325
|
+
level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
|
|
326
|
+
level3DSunFlare = undefined;
|
|
327
|
+
}
|
|
328
|
+
else if (!level3DSunHasFlare())
|
|
329
|
+
level3DSunFlare = new LensFlare3D;
|
|
330
|
+
}
|
|
331
|
+
const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
|
|
332
|
+
|
|
315
333
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
316
334
|
function level3DVector(value, fallback)
|
|
317
335
|
{
|
|
@@ -660,7 +678,8 @@ level3DAddType('Cylinder', function(pos, properties)
|
|
|
660
678
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
661
679
|
{color: WHITE, tile: -1, solid: true});
|
|
662
680
|
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
663
|
-
// object's rotation aims it, softness is how much of the cone fades,
|
|
681
|
+
// object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
|
|
682
|
+
// lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
|
|
664
683
|
level3DAddType('Light', function(pos, properties)
|
|
665
684
|
{
|
|
666
685
|
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
@@ -668,5 +687,6 @@ level3DAddType('Light', function(pos, properties)
|
|
|
668
687
|
light.coneSoftness = properties.softness;
|
|
669
688
|
if (properties.shadows && render3D)
|
|
670
689
|
render3D.shadowLight = light;
|
|
690
|
+
properties.lensFlare && light.addFlare();
|
|
671
691
|
return light;
|
|
672
|
-
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
692
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
|
|
@@ -89,7 +89,7 @@ class VoxelMap extends EngineObject3D
|
|
|
89
89
|
/** @type {Array<Vector3>} */
|
|
90
90
|
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
91
91
|
this.chunksChanged = new Set;
|
|
92
|
-
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
|
|
92
|
+
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
|
|
93
93
|
this.blockTypes = [];
|
|
94
94
|
this.tiles = new Map; // tile index to the uvs of its corners
|
|
95
95
|
|
|
@@ -161,27 +161,29 @@ class VoxelMap extends EngineObject3D
|
|
|
161
161
|
* @param {number} type - 1 to 255
|
|
162
162
|
* @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
|
|
163
163
|
* six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
|
|
164
|
-
* @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes
|
|
165
|
-
* leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
166
|
-
* transparent stage, and see-through too
|
|
167
|
-
|
|
164
|
+
* @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
|
|
165
|
+
* in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
166
|
+
* water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
|
|
167
|
+
* block as well, like water, whose surface then shows from under it */
|
|
168
|
+
setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
|
|
168
169
|
{
|
|
169
170
|
ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
|
|
170
171
|
const f = /** @type {any} */ (faces);
|
|
171
172
|
const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
|
|
172
173
|
[f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
|
|
173
174
|
ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
|
|
174
|
-
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
|
|
175
|
+
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
|
|
175
176
|
this.rebuild();
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
/** A block type's faces and how it is seen through
|
|
179
180
|
* @param {number} type
|
|
180
|
-
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
|
|
181
|
+
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
|
|
181
182
|
* @ignore */
|
|
182
183
|
blockType(type)
|
|
183
184
|
{
|
|
184
|
-
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false
|
|
185
|
+
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
|
|
186
|
+
doubleSided: false};
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
/** Build every chunk again, after changing data directly or ambientOcclusion */
|
|
@@ -251,6 +253,11 @@ class VoxelMap extends EngineObject3D
|
|
|
251
253
|
colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
|
|
252
254
|
}
|
|
253
255
|
mesh.addStrip(points, normal, uvs, colors);
|
|
256
|
+
|
|
257
|
+
// a double sided block shows the face from inside too, its corners the other way round
|
|
258
|
+
if (block.doubleSided)
|
|
259
|
+
mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
|
|
260
|
+
[uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
|
|
254
261
|
}
|
|
255
262
|
}
|
|
256
263
|
this.chunkMeshes[index]?.dispose();
|
package/src/engine.js
CHANGED
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
|
|
@@ -887,7 +887,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
///////////////////////////////////////////////////////////////////////////////
|
|
890
|
-
//
|
|
890
|
+
// ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
891
891
|
|
|
892
892
|
/** Generate and play a ZzFX sound
|
|
893
893
|
*
|
package/src/engineBuild.mjs
CHANGED
|
@@ -44,7 +44,6 @@ const enginePluginFiles =
|
|
|
44
44
|
`${PLUGIN_FOLDER}/newgrounds.js`,
|
|
45
45
|
`${PLUGIN_FOLDER}/postProcess.js`,
|
|
46
46
|
`${PLUGIN_FOLDER}/lightSystem.js`,
|
|
47
|
-
`${PLUGIN_FOLDER}/zzfxm.js`,
|
|
48
47
|
`${PLUGIN_FOLDER}/audioEffects.js`,
|
|
49
48
|
`${PLUGIN_FOLDER}/uiSystem.js`,
|
|
50
49
|
`${PLUGIN_FOLDER}/box2d.js`,
|
package/src/engineEditor.js
CHANGED
|
@@ -570,8 +570,8 @@ function editorMapRestore(map)
|
|
|
570
570
|
if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
|
|
571
571
|
editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
|
|
572
572
|
editorDiscardPending(record);
|
|
573
|
-
else if (!sameFile || !
|
|
574
|
-
!editorCopyData(data, saved.layers))
|
|
573
|
+
else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
|
|
574
|
+
!editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
|
|
575
575
|
{
|
|
576
576
|
record.pending = saved;
|
|
577
577
|
console.warn(`LittleJS editor: ${record.fileName} changed since its autosaved edits, ` +
|
|
@@ -870,6 +870,11 @@ function editorCopyData(data, saved)
|
|
|
870
870
|
return true;
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
+
// are an autosave's tiles a layer for each of the map's, each of its own size, so the map is resized only for tiles
|
|
874
|
+
// that fit it
|
|
875
|
+
const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) && saved.layers.length === count &&
|
|
876
|
+
saved.layers.every((layer)=> isArray(layer) && layer.length === width * height);
|
|
877
|
+
|
|
873
878
|
// remember a map's tile data and objects, or forget them when they are back to the file
|
|
874
879
|
function editorAutosave(record)
|
|
875
880
|
{
|
|
@@ -898,7 +903,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
898
903
|
{
|
|
899
904
|
if (!editorHas('onRestart')) return;
|
|
900
905
|
const before = editorMapSnapshot(record);
|
|
901
|
-
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
906
|
+
editorSetMapSnapshot(record, {width, height, layers: data, objects}, keepUnknown);
|
|
902
907
|
editorMapChanged(record, before);
|
|
903
908
|
return;
|
|
904
909
|
}
|
|
@@ -1015,13 +1020,15 @@ function editorMapSnapshot(record)
|
|
|
1015
1020
|
return editorObjectsCopy({width: map.width, height: map.height, layers: editorTileLayerData(map.layers), objects});
|
|
1016
1021
|
}
|
|
1017
1022
|
|
|
1018
|
-
// give a map a size, tiles and objects, the game's Restart hook makes its layers again
|
|
1019
|
-
|
|
1023
|
+
// give a map a size, tiles and objects, the game's Restart hook makes its layers again; keepUnknown leaves an object
|
|
1024
|
+
// layer past the snapshot's as it is, as applying an autosave from before the file had it does
|
|
1025
|
+
function editorSetMapSnapshot(record, snapshot, keepUnknown=false)
|
|
1020
1026
|
{
|
|
1021
1027
|
const map = record.map;
|
|
1022
1028
|
editorResizeMap(map, snapshot.width, snapshot.height);
|
|
1023
1029
|
editorCopyData(editorTileLayerData(map.layers), snapshot.layers);
|
|
1024
1030
|
editorRestoreObjects(map, snapshot);
|
|
1031
|
+
if (!keepUnknown)
|
|
1025
1032
|
for (const group of editorObjectGroups(map.layers).slice(snapshot.objects.length))
|
|
1026
1033
|
group.objects = []; // an Objects layer the editor made since
|
|
1027
1034
|
editorClearSelections();
|
|
@@ -2181,9 +2188,13 @@ function editorPaletteTiles(layer)
|
|
|
2181
2188
|
if (!image) return [];
|
|
2182
2189
|
if (layer.palette?.image === image) return layer.palette.tiles;
|
|
2183
2190
|
|
|
2184
|
-
|
|
2191
|
+
// a sheet read by its columns, a tileset with a margin or a spacing, is read with frame(), which asserts the
|
|
2192
|
+
// frame is inside the image, so a row past the image's end is found before its frame is made
|
|
2193
|
+
const tiles = [], base = live.tileInfo, columns = base.columns;
|
|
2185
2194
|
for (let i = 0; i < 4096; ++i)
|
|
2186
2195
|
{
|
|
2196
|
+
if (columns && base.pos.y + (i / columns | 0) * (base.size.y + base.padding*2) + base.size.y > image.height)
|
|
2197
|
+
break;
|
|
2187
2198
|
const t = editorTileInfo(live, i);
|
|
2188
2199
|
if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
|
|
2189
2200
|
tiles.push({tile: i, tileInfo: t});
|
package/src/engineExport.js
CHANGED
package/src/engineSettings.js
CHANGED
|
@@ -625,7 +625,11 @@ function setGLEnable(enable)
|
|
|
625
625
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
626
626
|
* @param {number} sides
|
|
627
627
|
* @memberof Settings */
|
|
628
|
-
function setGLCircleSides(sides)
|
|
628
|
+
function setGLCircleSides(sides)
|
|
629
|
+
{
|
|
630
|
+
ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
|
|
631
|
+
glCircleSides = sides;
|
|
632
|
+
}
|
|
629
633
|
|
|
630
634
|
/** Set default size of tiles in pixels
|
|
631
635
|
* @param {Vector2} size
|
package/src/engineTileLayer.js
CHANGED
|
@@ -128,6 +128,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
128
128
|
* Load tile layers from exported data
|
|
129
129
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
130
130
|
* tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
|
|
131
|
+
* - A tileset kept in the map with a margin or a spacing, a sheet with gaps between its tiles, is read where its
|
|
132
|
+
* tiles are, whatever padding the tile info has; one in a file of its own (a tsx) is not read, pass a tile info
|
|
133
|
+
* with its padding
|
|
134
|
+
* - An LDtk level loads through tileLayersFromLDtk, which makes it a map like these
|
|
131
135
|
* - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
|
|
132
136
|
* (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
|
|
133
137
|
* visibility carry to the layers inside it
|
|
@@ -137,7 +141,7 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
137
141
|
* @param {Object} tileMapData - Level data from exported data
|
|
138
142
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
|
|
139
143
|
* @param {number} [renderOrder] - Render order of the top layer
|
|
140
|
-
* @param {number}
|
|
144
|
+
* @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
|
|
141
145
|
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
142
146
|
* @return {Array<TileCollisionLayer>}
|
|
143
147
|
* @memberof TileLayers */
|
|
@@ -160,6 +164,19 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
160
164
|
ASSERT(tileMapData.width && tileMapData.height);
|
|
161
165
|
ASSERT(tileMapData.layers && tileMapData.layers.length);
|
|
162
166
|
|
|
167
|
+
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
168
|
+
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
169
|
+
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
170
|
+
const tileset = tileMapData.tilesets?.[0];
|
|
171
|
+
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
172
|
+
{
|
|
173
|
+
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
174
|
+
const width = tileInfo.textureInfo?.size.x || 0;
|
|
175
|
+
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
176
|
+
const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
|
|
177
|
+
tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
164
181
|
/** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
|
|
165
182
|
const layers = [];
|
|
@@ -188,6 +205,14 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
188
205
|
};
|
|
189
206
|
addLayers(tileMapData.layers, WHITE, true);
|
|
190
207
|
|
|
208
|
+
// a layer named for collision is the first tile layer of that name, which stays the same when layers are added
|
|
209
|
+
if (typeof collisionLayer === 'string')
|
|
210
|
+
{
|
|
211
|
+
const name = collisionLayer;
|
|
212
|
+
collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
|
|
213
|
+
ASSERT(collisionLayer >= 0, 'no tile layer named ' + name);
|
|
214
|
+
}
|
|
215
|
+
|
|
191
216
|
// create tile layers and fill with data
|
|
192
217
|
const tileLayers = [];
|
|
193
218
|
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
@@ -237,6 +262,134 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
237
262
|
function tileLayersColor(hex)
|
|
238
263
|
{ return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
|
|
239
264
|
|
|
265
|
+
// the Tiled property type of an LDtk field type, for the ones Tiled has a property for
|
|
266
|
+
const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
|
|
267
|
+
Color: 'color', FilePath: 'file'};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
271
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
|
|
272
|
+
* tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
|
|
273
|
+
* (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
|
|
274
|
+
* layer's, named with it, like Ground .25
|
|
275
|
+
* - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
|
|
276
|
+
* make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
|
|
277
|
+
* the stacked and see-through layers change the indices from level to level:
|
|
278
|
+
* tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
|
|
279
|
+
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
280
|
+
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
281
|
+
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
282
|
+
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
283
|
+
* another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
|
|
284
|
+
* keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
|
|
285
|
+
* - Layer offsets are not read
|
|
286
|
+
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
287
|
+
* @param {Object} ldtk - The LDtk project, its JSON
|
|
288
|
+
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
289
|
+
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
290
|
+
* @example
|
|
291
|
+
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
292
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
|
|
293
|
+
* objectLayersLoad(map);
|
|
294
|
+
* @memberof TileLayers */
|
|
295
|
+
function tileLayersFromLDtk(ldtk, level=0)
|
|
296
|
+
{
|
|
297
|
+
const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
|
|
298
|
+
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
299
|
+
if (!data)
|
|
300
|
+
throw new Error('tileLayersFromLDtk: no level ' + level);
|
|
301
|
+
if (!isArray(data.layerInstances))
|
|
302
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
|
|
303
|
+
const instances = data.layerInstances;
|
|
304
|
+
const tiled = (l)=> l.__type !== 'Entities';
|
|
305
|
+
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
306
|
+
const grid = first?.__gridSize || 16;
|
|
307
|
+
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
308
|
+
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
309
|
+
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
310
|
+
|
|
311
|
+
// the tileset of the first layer that has one, where its tiles are in its image
|
|
312
|
+
const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
|
|
313
|
+
if (tileset)
|
|
314
|
+
map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
|
|
315
|
+
image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
|
|
316
|
+
tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
|
|
317
|
+
spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
|
|
318
|
+
|
|
319
|
+
// LDtk lists the top layer first, Tiled the bottom one
|
|
320
|
+
for (let i = instances.length; i--;)
|
|
321
|
+
{
|
|
322
|
+
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
323
|
+
if (!tiled(instance))
|
|
324
|
+
{
|
|
325
|
+
const objects = (instance.entityInstances || []).map((entity)=>
|
|
326
|
+
{
|
|
327
|
+
// an entity is placed by its pivot, an object here by its middle
|
|
328
|
+
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
329
|
+
const properties = [];
|
|
330
|
+
for (const field of entity.fieldInstances || [])
|
|
331
|
+
{
|
|
332
|
+
const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
|
|
333
|
+
type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
|
|
334
|
+
}
|
|
335
|
+
return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
|
|
336
|
+
y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
|
|
337
|
+
});
|
|
338
|
+
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
|
|
342
|
+
const ownTiles = !tiles.length || !tileset ||
|
|
343
|
+
instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
|
|
344
|
+
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
|
|
345
|
+
{
|
|
346
|
+
debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
|
|
350
|
+
const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
|
|
351
|
+
const values = instance.__type === 'IntGrid';
|
|
352
|
+
if (values)
|
|
353
|
+
{
|
|
354
|
+
const data = empty();
|
|
355
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
|
|
356
|
+
map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
|
|
357
|
+
if (!tiles.length) continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// a layer for each depth in a cell and each opacity, the first tiles drawn solid first
|
|
361
|
+
const tilesName = values ? name + ' tiles' : name;
|
|
362
|
+
const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
|
|
363
|
+
{
|
|
364
|
+
let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
|
|
365
|
+
s || stacks.push(s = {depth, alpha, data: empty()});
|
|
366
|
+
return s.data;
|
|
367
|
+
};
|
|
368
|
+
{
|
|
369
|
+
// a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
|
|
370
|
+
// Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
|
|
371
|
+
// cell is the bottom one and each after it goes a layer higher
|
|
372
|
+
const depth = new Uint16Array(width * height);
|
|
373
|
+
for (const t of tiles)
|
|
374
|
+
{
|
|
375
|
+
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
376
|
+
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
377
|
+
stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
|
|
378
|
+
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const visible = instance.visible !== false;
|
|
382
|
+
stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
|
|
383
|
+
for (const {depth, alpha, data} of stacks)
|
|
384
|
+
{
|
|
385
|
+
const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
|
|
386
|
+
map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
|
|
387
|
+
width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return map;
|
|
391
|
+
}
|
|
392
|
+
|
|
240
393
|
///////////////////////////////////////////////////////////////////////////////
|
|
241
394
|
// Object layers
|
|
242
395
|
|
package/plugins/zzfxm.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS ZzFXM Plugin
|
|
3
|
-
* - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
|
|
4
|
-
* https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
|
|
5
|
-
* @namespace ZzFXM
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use strict';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Music Object - Stores a zzfx music track for later use
|
|
12
|
-
*
|
|
13
|
-
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
14
|
-
* @extends Sound
|
|
15
|
-
* @memberof ZzFXM
|
|
16
|
-
* @example
|
|
17
|
-
* // create some music
|
|
18
|
-
* const music_example = new ZzFXMusic(
|
|
19
|
-
* [
|
|
20
|
-
* [ // instruments
|
|
21
|
-
* [,0,400] // simple note
|
|
22
|
-
* ],
|
|
23
|
-
* [ // patterns
|
|
24
|
-
* [ // pattern 1
|
|
25
|
-
* [ // channel 0
|
|
26
|
-
* 0, -1, // instrument 0, left speaker
|
|
27
|
-
* 1, 0, 9, 1 // channel notes
|
|
28
|
-
* ],
|
|
29
|
-
* [ // channel 1
|
|
30
|
-
* 0, 1, // instrument 0, right speaker
|
|
31
|
-
* 0, 12, 17, -1 // channel notes
|
|
32
|
-
* ]
|
|
33
|
-
* ],
|
|
34
|
-
* ],
|
|
35
|
-
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
36
|
-
* 90 // BPM
|
|
37
|
-
* ]);
|
|
38
|
-
*
|
|
39
|
-
* // play the music on a loop
|
|
40
|
-
* music_example.playMusic();
|
|
41
|
-
*/
|
|
42
|
-
class ZzFXMusic extends Sound
|
|
43
|
-
{
|
|
44
|
-
/** Create a music object and cache the zzfx music samples for later use
|
|
45
|
-
* @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
|
|
46
|
-
*/
|
|
47
|
-
constructor(zzfxMusic)
|
|
48
|
-
{
|
|
49
|
-
super(undefined);
|
|
50
|
-
|
|
51
|
-
if (!soundEnable || headlessMode) return;
|
|
52
|
-
const [instruments, patterns, sequence, BPM] = zzfxMusic;
|
|
53
|
-
super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
|
|
54
|
-
this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
|
|
55
|
-
this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
60
|
-
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
61
|
-
|
|
62
|
-
/** Generate samples for a ZzFM song with given parameters
|
|
63
|
-
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
64
|
-
* @param {Array} patterns - Array of pattern data
|
|
65
|
-
* @param {Array} sequence - Array of pattern indexes
|
|
66
|
-
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
67
|
-
* @return {Array} - Left and right channel sample data
|
|
68
|
-
* @memberof ZzFXM */
|
|
69
|
-
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
70
|
-
{
|
|
71
|
-
let i, j, k;
|
|
72
|
-
let instrumentParameters;
|
|
73
|
-
let note;
|
|
74
|
-
let sample;
|
|
75
|
-
let patternChannel;
|
|
76
|
-
let notFirstBeat;
|
|
77
|
-
let stop;
|
|
78
|
-
let instrument;
|
|
79
|
-
let attenuation;
|
|
80
|
-
let outSampleOffset;
|
|
81
|
-
let isSequenceEnd;
|
|
82
|
-
let sampleOffset = 0;
|
|
83
|
-
let nextSampleOffset;
|
|
84
|
-
let sampleBuffer = [];
|
|
85
|
-
let leftChannelBuffer = [];
|
|
86
|
-
let rightChannelBuffer = [];
|
|
87
|
-
let channelIndex = 0;
|
|
88
|
-
let panning = 0;
|
|
89
|
-
let hasMore = 1;
|
|
90
|
-
let sampleCache = {};
|
|
91
|
-
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
92
|
-
|
|
93
|
-
// for each channel in order until there are no more
|
|
94
|
-
for (; hasMore; channelIndex++) {
|
|
95
|
-
|
|
96
|
-
// reset current values
|
|
97
|
-
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
98
|
-
|
|
99
|
-
// for each pattern in sequence
|
|
100
|
-
sequence.forEach((patternIndex, sequenceIndex)=> {
|
|
101
|
-
// get pattern for current channel, use empty 1 note pattern if none found
|
|
102
|
-
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
103
|
-
|
|
104
|
-
// check if there are more channels
|
|
105
|
-
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
106
|
-
|
|
107
|
-
// get next offset, use the length of first channel
|
|
108
|
-
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
109
|
-
// for each beat in pattern, plus one extra if end of sequence
|
|
110
|
-
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
111
|
-
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
112
|
-
|
|
113
|
-
// <channel-note>
|
|
114
|
-
note = patternChannel[i];
|
|
115
|
-
|
|
116
|
-
// stop if end, different instrument or new note
|
|
117
|
-
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
118
|
-
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
119
|
-
|
|
120
|
-
// fill buffer with samples for previous beat, most cpu intensive part
|
|
121
|
-
for (j = 0; j < beatLength && notFirstBeat;
|
|
122
|
-
|
|
123
|
-
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
124
|
-
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
125
|
-
) {
|
|
126
|
-
// copy sample to stereo buffers with panning
|
|
127
|
-
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
128
|
-
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
129
|
-
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// set up for next note
|
|
133
|
-
if (note) {
|
|
134
|
-
// set attenuation
|
|
135
|
-
attenuation = note % 1;
|
|
136
|
-
panning = patternChannel[1] || 0;
|
|
137
|
-
if (note |= 0) {
|
|
138
|
-
// get cached sample
|
|
139
|
-
sampleBuffer = sampleCache[
|
|
140
|
-
[
|
|
141
|
-
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
142
|
-
note
|
|
143
|
-
]
|
|
144
|
-
] = sampleCache[[instrument, note]] || (
|
|
145
|
-
// add sample to cache
|
|
146
|
-
instrumentParameters = [...instruments[instrument]],
|
|
147
|
-
instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
|
|
148
|
-
|
|
149
|
-
// allow negative values to stop notes
|
|
150
|
-
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// update the sample offset
|
|
157
|
-
outSampleOffset = nextSampleOffset;
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return [leftChannelBuffer, rightChannelBuffer];
|
|
162
|
-
}
|