littlejsengine 1.24.0 → 1.24.2
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 +86 -1
- package/EDITOR.md +2 -0
- package/FAQ.md +13 -2
- package/README.md +2 -2
- package/REFERENCE.md +27 -10
- package/dist/littlejs.d.ts +104 -29
- package/dist/littlejs.esm.js +2499 -1979
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2496 -1979
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2438 -1983
- package/package.json +1 -1
- package/plugins/box2d.js +1 -1
- package/plugins/gltf.js +62 -9
- package/plugins/lightSystem.js +1 -1
- package/plugins/math3d.js +1014 -1013
- package/plugins/medalSystem.js +7 -11
- package/plugins/particleEffects.js +2 -2
- package/plugins/pathFinder.js +2 -2
- package/plugins/pluginExport.js +1 -0
- package/plugins/render3d.js +134 -57
- package/plugins/render3dEditor.js +28 -12
- package/plugins/render3dExtras.js +32 -17
- package/plugins/render3dLevel.js +21 -12
- package/plugins/render3dVoxels.js +15 -8
- package/plugins/textureSheet.js +97 -3
- package/plugins/tweakables.js +1 -1
- package/plugins/tweenSystem.js +2 -2
- package/plugins/uiSystem.js +6 -2
- package/src/engine.js +34 -11
- package/src/engineAudio.js +13 -3
- package/src/engineDebug.js +4 -1
- package/src/engineDraw.js +29 -16
- package/src/engineEditor.js +99 -56
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +17 -3
- package/src/engineMath.js +6 -2
- package/src/engineObject.js +675 -675
- package/src/engineParticles.js +1 -1
- package/src/engineSettings.js +21 -1
- package/src/engineTileLayer.js +142 -43
- package/src/engineUtilities.js +20 -8
- package/src/engineWebGL.js +12 -8
package/src/engineParticles.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
44
44
|
* 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
45
45
|
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
|
|
46
|
-
* .5,
|
|
46
|
+
* .5, true // randomness, collide
|
|
47
47
|
* );
|
|
48
48
|
*/
|
|
49
49
|
class ParticleEmitter extends EngineObject
|
package/src/engineSettings.js
CHANGED
|
@@ -418,6 +418,13 @@ let vibrateEnable = true;
|
|
|
418
418
|
* @memberof Settings */
|
|
419
419
|
let soundEnable = true;
|
|
420
420
|
|
|
421
|
+
/** Play sound with an iPhone's silent switch on, as media does; off by default, so the switch mutes the game as it
|
|
422
|
+
* does a ringtone, and the player's own music keeps playing beside it; Safari 16.4 and up, elsewhere it does nothing
|
|
423
|
+
* @type {boolean}
|
|
424
|
+
* @default
|
|
425
|
+
* @memberof Settings */
|
|
426
|
+
let soundIgnoreSilentSwitch = false;
|
|
427
|
+
|
|
421
428
|
/** Volume scale to apply to all sound, music and speech
|
|
422
429
|
* Use setSoundVolume to also update the audio master gain immediately
|
|
423
430
|
* @type {number}
|
|
@@ -625,7 +632,11 @@ function setGLEnable(enable)
|
|
|
625
632
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
626
633
|
* @param {number} sides
|
|
627
634
|
* @memberof Settings */
|
|
628
|
-
function setGLCircleSides(sides)
|
|
635
|
+
function setGLCircleSides(sides)
|
|
636
|
+
{
|
|
637
|
+
ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
|
|
638
|
+
glCircleSides = sides;
|
|
639
|
+
}
|
|
629
640
|
|
|
630
641
|
/** Set default size of tiles in pixels
|
|
631
642
|
* @param {Vector2} size
|
|
@@ -815,6 +826,15 @@ function setVibrateEnable(enable) { vibrateEnable = enable; }
|
|
|
815
826
|
* @memberof Settings */
|
|
816
827
|
function setSoundEnable(enable) { soundEnable = enable; }
|
|
817
828
|
|
|
829
|
+
/** Set if sound plays with an iPhone's silent switch on, as media does, which also pauses the player's own music
|
|
830
|
+
* @param {boolean} ignore
|
|
831
|
+
* @memberof Settings */
|
|
832
|
+
function setSoundIgnoreSilentSwitch(ignore)
|
|
833
|
+
{
|
|
834
|
+
soundIgnoreSilentSwitch = ignore;
|
|
835
|
+
audioSetSession();
|
|
836
|
+
}
|
|
837
|
+
|
|
818
838
|
/** Set volume scale to apply to all sound, music and speech
|
|
819
839
|
* @param {number} volume
|
|
820
840
|
* @memberof Settings */
|
package/src/engineTileLayer.js
CHANGED
|
@@ -124,6 +124,19 @@ function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnl
|
|
|
124
124
|
// Tiled's flip flags, horizontal, vertical and diagonal as bits 2, 1 and 0, as [direction, mirror]
|
|
125
125
|
const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [1,1]];
|
|
126
126
|
|
|
127
|
+
// the tile info a layer's tile info gives a tile: a tile set's tile, a frame of a sheet read by its columns, or a
|
|
128
|
+
// tile of its grid; undefined past the end of a tile set, or of a sheet read by its columns, as values kept only for
|
|
129
|
+
// collision can be, which the level editor reads the same way
|
|
130
|
+
function tileLayerTileInfo(t, tile)
|
|
131
|
+
{
|
|
132
|
+
if (!t) return;
|
|
133
|
+
if (t.tiles) return t.tiles[tile];
|
|
134
|
+
if (!t.columns) return t.index(tile);
|
|
135
|
+
if (t.textureInfo && t.pos.y + (tile / t.columns | 0) * (t.size.y + t.padding*2) + t.size.y > t.textureInfo.size.y)
|
|
136
|
+
return;
|
|
137
|
+
return t.frame(tile);
|
|
138
|
+
}
|
|
139
|
+
|
|
127
140
|
/**
|
|
128
141
|
* Load tile layers from exported data
|
|
129
142
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
@@ -141,9 +154,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
141
154
|
* @param {Object} tileMapData - Level data from exported data
|
|
142
155
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
|
|
143
156
|
* @param {number} [renderOrder] - Render order of the top layer
|
|
144
|
-
* @param {number}
|
|
157
|
+
* @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
|
|
145
158
|
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
146
|
-
* @return {Array<TileCollisionLayer>}
|
|
159
|
+
* @return {Array<TileCollisionLayer>} - It throws for a map that is not whole cells, or a layer whose tiles do not
|
|
160
|
+
* fill it, before it makes any layer
|
|
147
161
|
* @memberof TileLayers */
|
|
148
162
|
function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrder=0, collisionLayer, draw=true)
|
|
149
163
|
{
|
|
@@ -160,20 +174,26 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
160
174
|
// the editor, in debug builds, keeps the map as the source of its edits and brings back autosaved ones
|
|
161
175
|
tileMapData = editorMapRestore(tileMapData);
|
|
162
176
|
|
|
163
|
-
// validate the tile map data
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
// validate the tile map data: a size that is not whole cells, or tiles that do not fill it, is said in any build,
|
|
178
|
+
// since the layers and their loops are made from that size
|
|
179
|
+
const {width: mapWidth, height: mapHeight} = tileMapData;
|
|
180
|
+
if (!(Number.isInteger(mapWidth) && Number.isInteger(mapHeight) && mapWidth > 0 && mapHeight > 0))
|
|
181
|
+
throw new Error(`tileLayersLoad: a map is a whole number of cells across and down, not ${mapWidth} by ${mapHeight}`);
|
|
182
|
+
if (!isArray(tileMapData.layers) || !tileMapData.layers.length)
|
|
183
|
+
throw new Error('tileLayersLoad: a map has a list of layers');
|
|
166
184
|
|
|
167
185
|
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
168
186
|
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
169
187
|
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
188
|
+
// a tile set from loadTiles has each tile where it was packed, whatever the map says of its own image
|
|
170
189
|
const tileset = tileMapData.tilesets?.[0];
|
|
171
|
-
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
190
|
+
if (tileInfo && !tileInfo.tiles && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
172
191
|
{
|
|
173
192
|
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
174
193
|
const width = tileInfo.textureInfo?.size.x || 0;
|
|
175
194
|
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
176
|
-
|
|
195
|
+
const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
|
|
196
|
+
tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
177
197
|
}
|
|
178
198
|
|
|
179
199
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
@@ -204,17 +224,30 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
204
224
|
};
|
|
205
225
|
addLayers(tileMapData.layers, WHITE, true);
|
|
206
226
|
|
|
207
|
-
//
|
|
227
|
+
// a layer named for collision is the first tile layer of that name, which stays the same when layers are added
|
|
228
|
+
if (typeof collisionLayer === 'string')
|
|
229
|
+
{
|
|
230
|
+
const name = collisionLayer;
|
|
231
|
+
collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
|
|
232
|
+
collisionLayer < 0 && console.error('tileLayersLoad: no tile layer is named ' + name + ', none is solid');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// create tile layers and fill with data, every layer's tiles checked first, so a bad one leaves none made
|
|
208
236
|
const tileLayers = [];
|
|
209
237
|
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
210
238
|
const layerCount = layers.length;
|
|
239
|
+
for (const [layerIndex, {dataLayer, color}] of layers.entries())
|
|
240
|
+
{
|
|
241
|
+
const tiles = dataLayer.data; // a list of gids, or a typed array of them
|
|
242
|
+
if (color && (!(isArray(tiles) || ArrayBuffer.isView(tiles)) || dataLayer.data.length !== levelSize.area()))
|
|
243
|
+
throw new Error(`tileLayersLoad: layer ${dataLayer.name ?? layerIndex} has ${dataLayer.data?.length} tiles for a map of ` +
|
|
244
|
+
`${levelSize.area()}; infinite maps and compressed layers are not read`);
|
|
245
|
+
}
|
|
211
246
|
for (let layerIndex=layerCount; layerIndex--;)
|
|
212
247
|
{
|
|
213
248
|
const {dataLayer, color: layerColor, visible} = layers[layerIndex];
|
|
214
249
|
if (!layerColor)
|
|
215
250
|
continue;
|
|
216
|
-
ASSERT(dataLayer.data && dataLayer.data.length, 'tile layer has no data, infinite maps and compressed layers are not supported');
|
|
217
|
-
ASSERT(levelSize.area() === dataLayer.data.length);
|
|
218
251
|
|
|
219
252
|
const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
|
|
220
253
|
const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
|
|
@@ -223,8 +256,8 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
223
256
|
if (!visible)
|
|
224
257
|
tileLayer.render = ()=> {}; // a hidden layer keeps its tiles and collision but is not drawn
|
|
225
258
|
|
|
226
|
-
for (let x=levelSize.x; x
|
|
227
|
-
for (let y=levelSize.y; y
|
|
259
|
+
for (let x=0; x<levelSize.x; ++x)
|
|
260
|
+
for (let y=0; y<levelSize.y; ++y)
|
|
228
261
|
{
|
|
229
262
|
const pos = vec2(x, levelSize.y-1-y);
|
|
230
263
|
const data = dataLayer.data[x + y*levelSize.x];
|
|
@@ -259,36 +292,45 @@ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: '
|
|
|
259
292
|
|
|
260
293
|
/**
|
|
261
294
|
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
262
|
-
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* - An IntGrid layer
|
|
295
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
|
|
296
|
+
* tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
|
|
297
|
+
* (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
|
|
298
|
+
* layer's, named with it, like Ground .25
|
|
299
|
+
* - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
|
|
300
|
+
* make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
|
|
301
|
+
* the stacked and see-through layers change the indices from level to level:
|
|
302
|
+
* tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
|
|
267
303
|
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
268
304
|
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
269
305
|
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
270
306
|
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
271
|
-
* another grid size or another tileset is left out, with a warning in debug builds
|
|
307
|
+
* another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
|
|
308
|
+
* keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
|
|
309
|
+
* - Layer offsets are not read
|
|
272
310
|
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
273
311
|
* @param {Object} ldtk - The LDtk project, its JSON
|
|
274
312
|
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
275
313
|
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
276
314
|
* @example
|
|
277
315
|
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
278
|
-
* const layers = tileLayersLoad(map, tile(0, 16), 0,
|
|
316
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
|
|
279
317
|
* objectLayersLoad(map);
|
|
280
318
|
* @memberof TileLayers */
|
|
281
319
|
function tileLayersFromLDtk(ldtk, level=0)
|
|
282
320
|
{
|
|
283
|
-
const levels = ldtk?.levels || [];
|
|
321
|
+
const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
|
|
284
322
|
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
285
|
-
|
|
286
|
-
|
|
323
|
+
if (!data)
|
|
324
|
+
throw new Error('tileLayersFromLDtk: no level ' + level);
|
|
325
|
+
if (!isArray(data.layerInstances))
|
|
326
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
|
|
287
327
|
const instances = data.layerInstances;
|
|
288
328
|
const tiled = (l)=> l.__type !== 'Entities';
|
|
289
329
|
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
290
330
|
const grid = first?.__gridSize || 16;
|
|
291
331
|
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
332
|
+
if (!(width > 0 && height > 0 && width * height <= 1 << 22))
|
|
333
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' is ' + width + ' by ' + height + ' cells');
|
|
292
334
|
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
293
335
|
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
294
336
|
|
|
@@ -306,7 +348,7 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
306
348
|
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
307
349
|
if (!tiled(instance))
|
|
308
350
|
{
|
|
309
|
-
const objects = (instance.entityInstances || []).map((entity)=>
|
|
351
|
+
const objects = (instance.entityInstances || []).filter((entity)=> isArray(entity.px)).map((entity)=>
|
|
310
352
|
{
|
|
311
353
|
// an entity is placed by its pivot, an object here by its middle
|
|
312
354
|
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
@@ -322,34 +364,64 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
322
364
|
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
323
365
|
continue;
|
|
324
366
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
(
|
|
328
|
-
|
|
367
|
+
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height)
|
|
368
|
+
{
|
|
369
|
+
console.warn(`tileLayersFromLDtk: layer ${name} has another grid, left out`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
// an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
|
|
373
|
+
const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
|
|
374
|
+
const values = instance.__type === 'IntGrid';
|
|
375
|
+
if (values)
|
|
376
|
+
{
|
|
377
|
+
const data = empty();
|
|
378
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
|
|
379
|
+
map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
|
|
380
|
+
}
|
|
381
|
+
// its tiles, those with a place, from the first layer's tileset
|
|
382
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])].filter((t)=> isArray(t.px));
|
|
383
|
+
if (!tiles.length)
|
|
384
|
+
{
|
|
385
|
+
// a tile layer with none yet is an empty layer, to paint in the editor, and a level of one still loads
|
|
386
|
+
values || map.layers.push({id, name, type: 'tilelayer', width, height, data: empty(), opacity,
|
|
387
|
+
visible: instance.visible !== false, x: 0, y: 0});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (tileset && (instance.__tilesetDefUid !== tileset.uid || tileset.tileGridSize !== grid))
|
|
329
391
|
{
|
|
330
|
-
|
|
392
|
+
console.warn(`tileLayersFromLDtk: the tiles of layer ${name} are on another tileset, left out`);
|
|
331
393
|
continue;
|
|
332
394
|
}
|
|
333
|
-
|
|
334
|
-
|
|
395
|
+
|
|
396
|
+
// a layer for each depth in a cell and each opacity, the first tiles drawn solid first
|
|
397
|
+
const tilesName = values ? name + ' tiles' : name;
|
|
398
|
+
const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
|
|
399
|
+
{
|
|
400
|
+
let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
|
|
401
|
+
s || stacks.push(s = {depth, alpha, data: empty()});
|
|
402
|
+
return s.data;
|
|
403
|
+
};
|
|
335
404
|
{
|
|
336
|
-
// a tile's place in pixels, its tile in the sheet,
|
|
337
|
-
// Tiled's two top bits; LDtk lists them in the order it draws them, so the first in a
|
|
338
|
-
// bottom one and each after it goes a layer higher
|
|
405
|
+
// a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
|
|
406
|
+
// Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
|
|
407
|
+
// cell is the bottom one and each after it goes a layer higher
|
|
339
408
|
const depth = new Uint16Array(width * height);
|
|
340
409
|
for (const t of tiles)
|
|
341
410
|
{
|
|
342
411
|
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
343
412
|
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
344
|
-
(
|
|
413
|
+
stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
|
|
345
414
|
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
346
415
|
}
|
|
347
416
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
417
|
+
const visible = instance.visible !== false;
|
|
418
|
+
stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
|
|
419
|
+
for (const {depth, alpha, data} of stacks)
|
|
420
|
+
{
|
|
421
|
+
const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
|
|
422
|
+
map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
|
|
423
|
+
width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
|
|
424
|
+
}
|
|
353
425
|
}
|
|
354
426
|
return map;
|
|
355
427
|
}
|
|
@@ -456,7 +528,15 @@ function objectLayersMake(tileMapData, object)
|
|
|
456
528
|
const pos = vec2(object.x / tilewidth, height - object.y / tileheight);
|
|
457
529
|
const {make} = type, result = make.prototype ? new make(pos) : make(pos);
|
|
458
530
|
if (!result || typeof result !== 'object') return;
|
|
459
|
-
|
|
531
|
+
for (const [key, value] of Object.entries(objectLayersProperties(type, object)))
|
|
532
|
+
{
|
|
533
|
+
// a property named as a method of the object, like update, would take the method's place, so it is left out
|
|
534
|
+
if (typeof result[key] === 'function' && typeof value !== 'function')
|
|
535
|
+
debug && console.warn(`objectLayersLoad: ${name} property ${key} has a method's name, left out`);
|
|
536
|
+
else
|
|
537
|
+
result[key] = value;
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
460
540
|
}
|
|
461
541
|
|
|
462
542
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -642,6 +722,7 @@ class TileLayer extends CanvasLayer
|
|
|
642
722
|
// set tile info
|
|
643
723
|
this.tileInfo = tileInfo.frame(0);
|
|
644
724
|
this.tileInfo.bleed = 0; // disable bleed for tile layers
|
|
725
|
+
this.tileInfo.tiles = tileInfo.tiles; // a tile set's list, filled in as its images load
|
|
645
726
|
}
|
|
646
727
|
|
|
647
728
|
// init tile data
|
|
@@ -821,8 +902,10 @@ class TileLayer extends CanvasLayer
|
|
|
821
902
|
const d = this.getData(layerPos);
|
|
822
903
|
if (!d || d.tile === undefined) return;
|
|
823
904
|
|
|
824
|
-
// a
|
|
825
|
-
|
|
905
|
+
// a tile set from loadTiles has each tile where it was packed, a tileset packed by loadSprite keeps its own
|
|
906
|
+
// columns, counted from its first tile, not the sheet's grid
|
|
907
|
+
const t = this.tileInfo, tileInfo = tileLayerTileInfo(t, d.tile);
|
|
908
|
+
if (t && !tileInfo) return; // past the end of the sheet or the tile set, or loading
|
|
826
909
|
this.drawLayerTile(drawPos, cellPixels, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
827
910
|
}
|
|
828
911
|
|
|
@@ -1097,7 +1180,23 @@ class TileCollisionLayer extends TileLayer
|
|
|
1097
1180
|
const tileData = this.getCollisionData(pos);
|
|
1098
1181
|
return tileData && collisionTest(tileData, vec2(pos.x + offset.x, pos.y + offset.y));
|
|
1099
1182
|
}
|
|
1100
|
-
|
|
1183
|
+
// only the part of the line over the layer, and a cell around it, can meet a tile, so a ray toward a point
|
|
1184
|
+
// far beyond is walked across the layer and no farther; the cell around keeps the step into the layer as it was
|
|
1185
|
+
const a = posStart.subtract(offset), d = posEnd.subtract(posStart);
|
|
1186
|
+
let t0 = 0, t1 = 1;
|
|
1187
|
+
for (const [p, q, high] of [[a.x, d.x, this.size.x + 1], [a.y, d.y, this.size.y + 1]])
|
|
1188
|
+
{
|
|
1189
|
+
if (!q)
|
|
1190
|
+
{
|
|
1191
|
+
if (p < -1 || p > high) return;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
const u = (-1 - p) / q, v = (high - p) / q;
|
|
1195
|
+
t0 = max(t0, min(u, v)), t1 = min(t1, max(u, v));
|
|
1196
|
+
}
|
|
1197
|
+
if (t0 > t1) return;
|
|
1198
|
+
const hitPos = lineTest(t0 ? a.add(d.scale(t0)) : a, t1 < 1 ? a.add(d.scale(t1)) : posEnd.subtract(offset),
|
|
1199
|
+
testFunction, normal);
|
|
1101
1200
|
if (hitPos)
|
|
1102
1201
|
hitPos.x += offset.x, hitPos.y += offset.y;
|
|
1103
1202
|
if (debugRaycast && hitPos)
|
package/src/engineUtilities.js
CHANGED
|
@@ -141,6 +141,7 @@ function saveText(text, filename='text', type='text/plain')
|
|
|
141
141
|
|
|
142
142
|
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
143
143
|
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
144
|
+
* - A browser with no OffscreenCanvas, Safari before 16.4, gets a canvas element that is not on the page
|
|
144
145
|
* @param {number} width - In pixels
|
|
145
146
|
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
146
147
|
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
@@ -149,9 +150,16 @@ function saveText(text, filename='text', type='text/plain')
|
|
|
149
150
|
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
150
151
|
{
|
|
151
152
|
ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
152
|
-
|
|
153
|
+
const canvas = typeof OffscreenCanvas != 'undefined' ? new OffscreenCanvas(width, height) :
|
|
154
|
+
Object.assign(document.createElement('canvas'), {width, height});
|
|
155
|
+
return /** @type {OffscreenCanvasRenderingContext2D} */ (canvas.getContext('2d', {willReadFrequently}));
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
// can a canvas be made to draw into: an OffscreenCanvas, or a canvas element where there is none, as in Safari before
|
|
159
|
+
// 16.4; neither in a headless test
|
|
160
|
+
function canvasAvailable()
|
|
161
|
+
{ return typeof OffscreenCanvas != 'undefined' || typeof document != 'undefined' && !!document.createElement; }
|
|
162
|
+
|
|
155
163
|
/** Save a canvas to disk
|
|
156
164
|
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
157
165
|
* @param {string} [filename]
|
|
@@ -159,7 +167,7 @@ function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
|
159
167
|
* @memberof Utilities */
|
|
160
168
|
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
161
169
|
{
|
|
162
|
-
if (canvas instanceof OffscreenCanvas)
|
|
170
|
+
if (typeof OffscreenCanvas != 'undefined' && canvas instanceof OffscreenCanvas)
|
|
163
171
|
{
|
|
164
172
|
// copy to temporary canvas and save
|
|
165
173
|
const saveCanvas = document.createElement('canvas');
|
|
@@ -169,7 +177,7 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
169
177
|
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
170
178
|
}
|
|
171
179
|
else
|
|
172
|
-
saveDataURL(canvas.toDataURL(type), filename);
|
|
180
|
+
saveDataURL(/** @type {HTMLCanvasElement} */ (canvas).toDataURL(type), filename);
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
/** Save a data url to disk
|
|
@@ -237,16 +245,19 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
237
245
|
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
/** Write save data to local storage
|
|
248
|
+
/** Write save data to local storage, an object as readSaveData gives it back
|
|
241
249
|
* @param {string} saveName - unique name for the game/save
|
|
242
250
|
* @param {object} saveData - object containing data to be saved
|
|
251
|
+
* @return {boolean} - Whether it was written, false when storage is unavailable or full
|
|
243
252
|
* @memberof Utilities */
|
|
244
253
|
function writeSaveData(saveName, saveData)
|
|
245
254
|
{
|
|
246
255
|
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
256
|
+
ASSERT(typeof saveData === 'object' && saveData !== null && !isArray(saveData),
|
|
257
|
+
'writeSaveData: save data must be an object, readSaveData reads it back into one');
|
|
247
258
|
// tolerate localStorage being unavailable or quota exceeded
|
|
248
|
-
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
249
|
-
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
259
|
+
try { localStorage.setItem(saveName, JSON.stringify(saveData)); return true; }
|
|
260
|
+
catch { LOG('writeSaveData: failed to write', saveName); return false; }
|
|
250
261
|
}
|
|
251
262
|
|
|
252
263
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -283,8 +294,9 @@ function noise2D(x, y)
|
|
|
283
294
|
{
|
|
284
295
|
const ix = floor(x), iy = floor(y);
|
|
285
296
|
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
286
|
-
// large prime decorrelates neighboring rows
|
|
287
|
-
|
|
297
|
+
// large prime decorrelates neighboring rows; multiplied as 32 bit integers, which is what the hash keeps of it,
|
|
298
|
+
// so the row stays exact however far out y is, where a plain product would lose its low bits
|
|
299
|
+
const h = (a, b) => noiseHash(a + Math.imul(b, 374761393));
|
|
288
300
|
return lerp(
|
|
289
301
|
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
290
302
|
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
package/src/engineWebGL.js
CHANGED
|
@@ -576,9 +576,9 @@ function glSetTextureData(texture, image, pixelated=tilesPixelated)
|
|
|
576
576
|
{
|
|
577
577
|
// WebGL ignores the unpack flag below for a bitmap and uploads it as it was made, so it is drawn onto a
|
|
578
578
|
// canvas first, which reads either kind of bitmap right and uploads as the flag says
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
image = canvas;
|
|
579
|
+
const context = createCanvasContext(image.width, image.height);
|
|
580
|
+
context.drawImage(image, 0, 0);
|
|
581
|
+
image = context.canvas;
|
|
582
582
|
}
|
|
583
583
|
// smooth filtering mixes a texel with its see through neighbors, right only for premultiplied color, or the
|
|
584
584
|
// edges go dark; pixel art is sampled a texel at a time, and uploads straight color as it always has
|
|
@@ -961,6 +961,9 @@ function glClearRect(x, y, width, height)
|
|
|
961
961
|
|
|
962
962
|
///////////////////////////////////////////////////////////////////////////////
|
|
963
963
|
|
|
964
|
+
// the strip glMakeOutline makes and the vectors it writes, kept for the next, as it is read into the batch first
|
|
965
|
+
const glOutlineStrip = [], glOutlinePool = [];
|
|
966
|
+
|
|
964
967
|
// WebGL internal function to convert polygon to outline triangle strip
|
|
965
968
|
function glMakeOutline(points, width, wrap=true)
|
|
966
969
|
{
|
|
@@ -968,7 +971,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
968
971
|
return [];
|
|
969
972
|
|
|
970
973
|
const halfWidth = width / 2;
|
|
971
|
-
const strip =
|
|
974
|
+
const strip = glOutlineStrip, pool = glOutlinePool;
|
|
975
|
+
strip.length = 0;
|
|
972
976
|
const n = points.length;
|
|
973
977
|
const e = 1e-6;
|
|
974
978
|
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
@@ -1025,10 +1029,10 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
1025
1029
|
}
|
|
1026
1030
|
|
|
1027
1031
|
// create inner and outer points along the normal
|
|
1028
|
-
const inner =
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
strip.push(outer);
|
|
1032
|
+
const inner = pool[strip.length] ||= vec2(), outer = pool[strip.length + 1] ||= vec2();
|
|
1033
|
+
inner.x = curr.x - nx * halfWidth, inner.y = curr.y - ny * halfWidth;
|
|
1034
|
+
outer.x = curr.x + nx * halfWidth, outer.y = curr.y + ny * halfWidth;
|
|
1035
|
+
strip.push(inner, outer);
|
|
1032
1036
|
}
|
|
1033
1037
|
if (strip.length > 1 && wrap)
|
|
1034
1038
|
{
|