littlejsengine 1.24.1 → 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 +59 -1
- package/EDITOR.md +2 -0
- package/FAQ.md +13 -2
- package/README.md +1 -1
- package/REFERENCE.md +20 -6
- package/dist/littlejs.d.ts +77 -13
- package/dist/littlejs.esm.js +2395 -1939
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2392 -1939
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2356 -1952
- package/package.json +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 +131 -55
- package/plugins/render3dEditor.js +24 -10
- package/plugins/render3dExtras.js +29 -17
- package/plugins/render3dLevel.js +21 -12
- package/plugins/render3dVoxels.js +2 -2
- 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 +82 -50
- 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 +16 -0
- package/src/engineTileLayer.js +85 -22
- package/src/engineUtilities.js +20 -8
- package/src/engineWebGL.js +12 -8
package/plugins/render3dLevel.js
CHANGED
|
@@ -198,18 +198,21 @@ function level3DTerrainShape(terrain)
|
|
|
198
198
|
{
|
|
199
199
|
if (!terrain || typeof terrain !== 'object') return;
|
|
200
200
|
const size = terrain.size, rows = terrain.heights, n = LEVEL3D_TERRAIN_SAMPLES;
|
|
201
|
-
if (!isArray(size) || size.length !== 2 || !size.every((v)=>
|
|
201
|
+
if (!isArray(size) || size.length !== 2 || !size.every((v)=> level3DFinite(v) && v > 0)) return;
|
|
202
202
|
if (!isArray(rows) || rows.length < 2 || rows.length > n || !isArray(rows[0])) return;
|
|
203
203
|
const columns = rows[0].length;
|
|
204
204
|
if (columns < 2 || columns > n || !rows.every((row)=> isArray(row) && row.length === columns)) return;
|
|
205
|
-
const heights = rows.map((row)=> row.map((v)=>
|
|
205
|
+
const heights = rows.map((row)=> row.map((v)=> level3DFinite(v) ? clamp(v) : 0));
|
|
206
206
|
return {pos: level3DVector(terrain.pos, vec3()), size: vec2(size[0], size[1]),
|
|
207
|
-
height:
|
|
207
|
+
height: level3DFinite(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// a hex color of a level as a Color, undefined when it is not one
|
|
211
211
|
function level3DHexColor(hex)
|
|
212
|
-
{ return /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex) ? new Color().setHex(hex) : undefined; }
|
|
212
|
+
{ return typeof hex === 'string' && /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex) ? new Color().setHex(hex) : undefined; }
|
|
213
|
+
|
|
214
|
+
// a number of a level that is a real one, not NaN or Infinity, which JSON writes as 1e999
|
|
215
|
+
const level3DFinite = (v)=> typeof v === 'number' && isFinite(v);
|
|
213
216
|
|
|
214
217
|
// the paint of a level's terrain block, for a terrain of so many samples: its colors as they are written and a
|
|
215
218
|
// color for each sample, row after row, 0 for none and 1 the first of the colors; undefined when it has none or
|
|
@@ -248,7 +251,7 @@ function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain
|
|
|
248
251
|
let level3DVoxelPlain;
|
|
249
252
|
function level3DVoxelPlainTiles()
|
|
250
253
|
{
|
|
251
|
-
if (textureInfos[0]?.size.x || headlessMode ||
|
|
254
|
+
if (textureInfos[0]?.size.x || headlessMode || !canvasAvailable())
|
|
252
255
|
return tile();
|
|
253
256
|
if (!level3DVoxelPlain)
|
|
254
257
|
{
|
|
@@ -289,12 +292,12 @@ function level3DSceneApply(scene)
|
|
|
289
292
|
{
|
|
290
293
|
const r = render3D;
|
|
291
294
|
if (!r || !scene || typeof scene !== 'object') return;
|
|
292
|
-
const color =
|
|
295
|
+
const color = level3DHexColor;
|
|
293
296
|
const sky = isArray(scene.sky) && scene.sky.length === 3 ? scene.sky.map(color) : [];
|
|
294
297
|
if (sky.length && sky.every((c)=> c))
|
|
295
298
|
{
|
|
296
299
|
// as setSky does, without disposing a dome that is the game's own
|
|
297
|
-
const [top, horizon, bottom] = sky, ambient =
|
|
300
|
+
const [top, horizon, bottom] = sky, ambient = level3DFinite(scene.ambient) ? scene.ambient : .5;
|
|
298
301
|
level3DSkies.has(r.sky) && r.sky.dispose();
|
|
299
302
|
level3DSkies.add(r.sky = buildSky(top, horizon, bottom));
|
|
300
303
|
r.fogColor = horizon.copy();
|
|
@@ -306,7 +309,7 @@ function level3DSceneApply(scene)
|
|
|
306
309
|
r.sunDirection = sun;
|
|
307
310
|
r.sunColor = color(scene.sunColor) || r.sunColor;
|
|
308
311
|
const fog = scene.fog;
|
|
309
|
-
if (isArray(fog) && fog.length === 2 && fog.every(
|
|
312
|
+
if (isArray(fog) && fog.length === 2 && fog.every(level3DFinite))
|
|
310
313
|
r.fogStart = fog[0], r.fogEnd = fog[1];
|
|
311
314
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
312
315
|
if (typeof scene.shadows === 'boolean')
|
|
@@ -333,7 +336,7 @@ const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3
|
|
|
333
336
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
334
337
|
function level3DVector(value, fallback)
|
|
335
338
|
{
|
|
336
|
-
return isArray(value) && value.length === 3 && value.every(
|
|
339
|
+
return isArray(value) && value.length === 3 && value.every(level3DFinite) ?
|
|
337
340
|
vec3(value[0], value[1], value[2]) : fallback;
|
|
338
341
|
}
|
|
339
342
|
|
|
@@ -349,12 +352,14 @@ function level3DProperties(type, object)
|
|
|
349
352
|
{
|
|
350
353
|
const d = type.defaults[key];
|
|
351
354
|
if (isColor(d))
|
|
352
|
-
|
|
355
|
+
level3DHexColor(value) && (properties[key] = level3DHexColor(value));
|
|
353
356
|
else if (isVector3(d))
|
|
354
357
|
properties[key] = level3DVector(value, properties[key]);
|
|
355
358
|
else if (isVector2(d))
|
|
356
|
-
isArray(value) && value.length === 2 && value.every(
|
|
359
|
+
isArray(value) && value.length === 2 && value.every(level3DFinite) &&
|
|
357
360
|
(properties[key] = vec2(value[0], value[1]));
|
|
361
|
+
else if (typeof d === 'number')
|
|
362
|
+
level3DFinite(value) && (properties[key] = value);
|
|
358
363
|
else if (d === undefined || typeof value === typeof d)
|
|
359
364
|
properties[key] = value;
|
|
360
365
|
}
|
|
@@ -439,6 +444,8 @@ const level3DPrefabs = new Map;
|
|
|
439
444
|
// how many prefabs deep an instance being made is, a prefab that holds itself is stopped; and how many of those
|
|
440
445
|
// are attached: a prefab inside an attached one is attached too, or its parts would be left behind in the world
|
|
441
446
|
let level3DPrefabDepth = 0, level3DPrefabAttached = 0;
|
|
447
|
+
// the prefabs being made, one inside the next, so one that holds itself is caught where it does, however many times
|
|
448
|
+
const level3DPrefabMaking = [];
|
|
442
449
|
|
|
443
450
|
/** Add a prefab: a small level, its objects placed about its own origin, to place many times under one name
|
|
444
451
|
* - It is a type from then on: a level's object of that type, the level editor's Place list and level3DSpawn make
|
|
@@ -580,11 +587,12 @@ class Prefab3D extends EngineObject3D
|
|
|
580
587
|
{
|
|
581
588
|
const prefab = level3DPrefabs.get(this.prefabName);
|
|
582
589
|
if (!prefab) return;
|
|
583
|
-
if (level3DPrefabDepth >= 8)
|
|
590
|
+
if (level3DPrefabMaking.includes(this.prefabName) || level3DPrefabDepth >= 8)
|
|
584
591
|
{
|
|
585
592
|
debug && console.error(`level3DLoad: the prefab ${this.prefabName} holds itself, left out there`);
|
|
586
593
|
return;
|
|
587
594
|
}
|
|
595
|
+
level3DPrefabMaking.push(this.prefabName);
|
|
588
596
|
++level3DPrefabDepth;
|
|
589
597
|
this.attached && ++level3DPrefabAttached;
|
|
590
598
|
try
|
|
@@ -628,6 +636,7 @@ class Prefab3D extends EngineObject3D
|
|
|
628
636
|
finally
|
|
629
637
|
{
|
|
630
638
|
--level3DPrefabDepth;
|
|
639
|
+
level3DPrefabMaking.pop();
|
|
631
640
|
this.attached && --level3DPrefabAttached;
|
|
632
641
|
}
|
|
633
642
|
}
|
|
@@ -256,8 +256,8 @@ class VoxelMap extends EngineObject3D
|
|
|
256
256
|
|
|
257
257
|
// a double sided block shows the face from inside too, its corners the other way round
|
|
258
258
|
if (block.doubleSided)
|
|
259
|
-
mesh.addStrip([points[
|
|
260
|
-
[uvs[
|
|
259
|
+
mesh.addStrip([points[0], points[2], points[1], points[3]], normal.scale(-1),
|
|
260
|
+
[uvs[0], uvs[2], uvs[1], uvs[3]], [colors[0], colors[2], colors[1], colors[3]]);
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
this.chunkMeshes[index]?.dispose();
|
package/plugins/textureSheet.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Animation frames keep layout and wrap across rows as needed
|
|
8
8
|
* - WebGL textures upload once per batch of loads
|
|
9
9
|
* - loadAtlas imports pre-packed atlases (TexturePacker and Aseprite json)
|
|
10
|
+
* - loadTiles packs separate tile images, or several tile sheets, into one tile set for tile layers and maps
|
|
10
11
|
* @namespace TextureSheets
|
|
11
12
|
*/
|
|
12
13
|
|
|
@@ -32,6 +33,10 @@ let textureSheets = [];
|
|
|
32
33
|
// pending loads pack through a queue so sheets fill in call order
|
|
33
34
|
let textureSheetQueue = Promise.resolve();
|
|
34
35
|
let textureSheetPendingCount = 0;
|
|
36
|
+
// what each load handed back by what it was given, so the same image loaded again is packed once
|
|
37
|
+
const textureSheetLoaded = new Map;
|
|
38
|
+
// a load's key: what it was given, a vector by its numbers
|
|
39
|
+
const textureSheetLoadKey = (...parts)=> parts.map((p)=> isVector2(p) ? p.x + ',' + p.y : String(p)).join('|');
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* Texture Sheet - A texture that images are packed into as they load
|
|
@@ -188,6 +193,7 @@ class TextureSheet
|
|
|
188
193
|
* - Pass frameSize for animations, then step through them with TileInfo.frame
|
|
189
194
|
* - Grid images keep their layout and frames wrap down to the next row
|
|
190
195
|
* - Pass sourcePadding if the source image has padding baked in around frames
|
|
196
|
+
* - The same image loaded again with the same settings gives back what the first load did, packed once
|
|
191
197
|
* @param {string} src - Image source path
|
|
192
198
|
* @param {Vector2|number} [frameSize] - Size of each animation frame in pixels, or the whole image less its
|
|
193
199
|
* source padding if not passed
|
|
@@ -208,10 +214,14 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
208
214
|
|
|
209
215
|
if (isNumber(frameSize))
|
|
210
216
|
frameSize = vec2(/** @type {number} */ (frameSize));
|
|
217
|
+
const key = textureSheetLoadKey('sprite', src, frameSize, padding, sourcePadding);
|
|
218
|
+
if (!headlessMode && textureSheetLoaded.has(key))
|
|
219
|
+
return textureSheetLoaded.get(key);
|
|
211
220
|
|
|
212
221
|
// start with an empty tile that gets filled in when the image loads
|
|
213
222
|
const tileInfo = new TileInfo(vec2(), vec2(), undefined, padding, 0);
|
|
214
223
|
if (headlessMode) return tileInfo;
|
|
224
|
+
textureSheetLoaded.set(key, tileInfo);
|
|
215
225
|
|
|
216
226
|
// point at a sheet right away so drawing before it loads picks up empty pixels
|
|
217
227
|
tileInfo.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
|
|
@@ -254,6 +264,70 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
254
264
|
return tileInfo;
|
|
255
265
|
}
|
|
256
266
|
|
|
267
|
+
/** Load tile images and pack them into texture sheets as one tile set, for tile layers and maps
|
|
268
|
+
* - Each image is cut into tiles of tileSize, left to right then down, so an image of one tile is one tile and a
|
|
269
|
+
* sheet is all of its tiles; the tiles are numbered from 0 in the order the images are given
|
|
270
|
+
* - Returns a tile set at once, a TileInfo whose tiles fill in as the images load; wait for them with spritesReady
|
|
271
|
+
* - Give it to tileLayersLoad, a TileLayer or a TileCollisionLayer as its tile info: tile n draws tiles[n],
|
|
272
|
+
* wherever it was packed, and the level editor's palette offers each of them; frame and index do not read the
|
|
273
|
+
* list, use set.tiles[n] for one tile; make the layers after spritesReady, a layer made before draws nothing
|
|
274
|
+
* - An image that is not a whole number of tiles gives the whole tiles in it; a sheet with gaps between its tiles
|
|
275
|
+
* is not read, cut it into its tiles first
|
|
276
|
+
* - An image that fails to load, or that no sheet can hold, adds no tiles and says so in the console, so the tiles
|
|
277
|
+
* of the images after it move up
|
|
278
|
+
* - The same images loaded again with the same settings give back what the first load did, packed once
|
|
279
|
+
* @param {Array<string>} sources - Image source paths
|
|
280
|
+
* @param {Vector2|number} [tileSize] - Size of a tile in pixels
|
|
281
|
+
* @param {number} [padding] - How many pixels padding around each tile on the sheet
|
|
282
|
+
* @return {TileInfo}
|
|
283
|
+
* @example
|
|
284
|
+
* const tiles = loadTiles(['grass.png', 'dirt.png', 'water.png', 'props.png'], 16);
|
|
285
|
+
* await spritesReady();
|
|
286
|
+
* tileLayersLoad(map, tiles); // tile 0 is grass, 1 dirt, 2 water, then the tiles of props.png
|
|
287
|
+
* @memberof TextureSheets */
|
|
288
|
+
function loadTiles(sources, tileSize=tileDefaultSize, padding=textureSheetPadding)
|
|
289
|
+
{
|
|
290
|
+
ASSERT(isArray(sources) && sources.every((src)=> isStringLike(src)), 'sources must be a list of image paths');
|
|
291
|
+
ASSERT(isVector2(tileSize) || isNumber(tileSize), 'tileSize must be a vec2 or number');
|
|
292
|
+
ASSERT(isNumber(padding), 'padding must be a number');
|
|
293
|
+
ASSERT(engineInitialized || headlessMode, 'call loadTiles after engineInit, e.g. in gameInit');
|
|
294
|
+
const size = isNumber(tileSize) ? vec2(/** @type {number} */ (tileSize)) : /** @type {Vector2} */ (tileSize).copy();
|
|
295
|
+
const key = textureSheetLoadKey('tiles', sources.join('\n'), size, padding);
|
|
296
|
+
if (!headlessMode && textureSheetLoaded.has(key))
|
|
297
|
+
return textureSheetLoaded.get(key);
|
|
298
|
+
const set = new TileInfo(vec2(), size, undefined, 0, 0);
|
|
299
|
+
set.tiles = [];
|
|
300
|
+
if (headlessMode) return set;
|
|
301
|
+
textureSheetLoaded.set(key, set);
|
|
302
|
+
set.textureInfo = (textureSheets[0] || textureSheetCreate()).textureInfo;
|
|
303
|
+
|
|
304
|
+
// every image decodes at once, and packs through the queue in the order given, so the tiles keep that order
|
|
305
|
+
for (const src of sources)
|
|
306
|
+
{
|
|
307
|
+
const image = new Image;
|
|
308
|
+
const imagePromise = new Promise((resolve)=>
|
|
309
|
+
{
|
|
310
|
+
image.onerror = image.onload = resolve;
|
|
311
|
+
image.crossOrigin = 'anonymous';
|
|
312
|
+
image.src = src;
|
|
313
|
+
});
|
|
314
|
+
textureSheetQueueJob('loadTiles ' + src, async ()=>
|
|
315
|
+
{
|
|
316
|
+
await imagePromise;
|
|
317
|
+
// the whole tiles of it, an edge past the last one left out
|
|
318
|
+
const columns = image.width / size.x | 0, rows = image.height / size.y | 0, count = columns * rows;
|
|
319
|
+
const added = count && textureSheetAdd(vec2(columns * size.x, rows * size.y), size, padding, 0);
|
|
320
|
+
if (!added)
|
|
321
|
+
return console.warn('loadTiles: ' + src + (count ? ' does not fit on a texture sheet' :
|
|
322
|
+
' failed to load, or is smaller than a tile') + ', its tiles are left out');
|
|
323
|
+
added.sheet.drawImage(image, added.tile, false); // upload once per batch
|
|
324
|
+
for (let k = 0; k < count; ++k)
|
|
325
|
+
set.tiles.push(added.tile.frame(k));
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return set;
|
|
329
|
+
}
|
|
330
|
+
|
|
257
331
|
/** Load a pre-packed texture atlas and repack it onto texture sheets
|
|
258
332
|
* - Supports TexturePacker json (hash and array) and Aseprite json
|
|
259
333
|
* - Returns an empty object which is filled with TileInfos when loaded
|
|
@@ -261,6 +335,7 @@ function loadSprite(src, frameSize, padding=textureSheetPadding, sourcePadding=0
|
|
|
261
335
|
* - Aseprite frame tags become animations, so do names like run_0, run_1
|
|
262
336
|
* - Trimmed frames are restored to their full source size when packed
|
|
263
337
|
* - Rotated frames are rotated back upright when packed
|
|
338
|
+
* - The same atlas loaded again by its paths, with the same padding, gives back what the first load did, packed once
|
|
264
339
|
* @param {string} imageSrc - Atlas image path
|
|
265
340
|
* @param {string|Object} jsonSrc - Atlas json path, or already parsed json data
|
|
266
341
|
* @param {number} [padding] - How many pixels padding around each frame
|
|
@@ -278,9 +353,15 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
278
353
|
ASSERT(isNumber(padding), 'padding must be a number');
|
|
279
354
|
ASSERT(engineInitialized || headlessMode, 'call loadAtlas after engineInit, e.g. in gameInit');
|
|
280
355
|
|
|
356
|
+
// json passed in as data may be another atlas each time, only one loaded by its path is kept
|
|
357
|
+
const key = typeof jsonSrc === 'object' ? undefined : textureSheetLoadKey('atlas', imageSrc, jsonSrc, padding);
|
|
358
|
+
if (!headlessMode && textureSheetLoaded.has(key))
|
|
359
|
+
return textureSheetLoaded.get(key);
|
|
360
|
+
|
|
281
361
|
/** @type {Object<string, TileInfo>} */
|
|
282
362
|
const atlas = {};
|
|
283
363
|
if (headlessMode) return atlas;
|
|
364
|
+
key && textureSheetLoaded.set(key, atlas);
|
|
284
365
|
|
|
285
366
|
// start fetching the json and decoding the image right away, in parallel
|
|
286
367
|
const jsonPromise = typeof jsonSrc === 'object' ? Promise.resolve(jsonSrc) :
|
|
@@ -401,12 +482,14 @@ function parseAtlas(data)
|
|
|
401
482
|
const tagged = new Set;
|
|
402
483
|
for (const tag of tags)
|
|
403
484
|
{
|
|
404
|
-
|
|
405
|
-
|
|
485
|
+
// a tag reaches only frames there are, whatever its numbers say
|
|
486
|
+
const from = max(0, floor(tag.from) || 0), to = min(frames.length - 1, floor(tag.to) || 0);
|
|
487
|
+
groups.push({name: tag.name, frames: frames.slice(from, to + 1)});
|
|
488
|
+
for (let i = from; i <= to; ++i)
|
|
406
489
|
tagged.add(i);
|
|
407
490
|
}
|
|
408
491
|
frames.forEach((f, i)=> tagged.has(i) || groups.push({name: f.name, frames: [f]}));
|
|
409
|
-
return groups;
|
|
492
|
+
return parseAtlasNamesCheck(groups);
|
|
410
493
|
}
|
|
411
494
|
|
|
412
495
|
// group frames that share a name stem with contiguous trailing numbers
|
|
@@ -435,6 +518,17 @@ function parseAtlas(data)
|
|
|
435
518
|
else
|
|
436
519
|
list.forEach(f=> groups.push({name: f.name, frames: [f]}));
|
|
437
520
|
}
|
|
521
|
+
return parseAtlasNamesCheck(groups);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// an atlas's groups, with a warning for each name two of them have, as a sprite is looked up by name and the second
|
|
525
|
+
// takes the first's place
|
|
526
|
+
function parseAtlasNamesCheck(groups)
|
|
527
|
+
{
|
|
528
|
+
const names = new Set;
|
|
529
|
+
for (const {name} of groups)
|
|
530
|
+
names.has(name) ? console.warn(`parseAtlas: two sprites are named ${name}, the second takes the first's place`) :
|
|
531
|
+
names.add(name);
|
|
438
532
|
return groups;
|
|
439
533
|
}
|
|
440
534
|
|
package/plugins/tweakables.js
CHANGED
|
@@ -379,7 +379,7 @@ function tweakNumber(parent, axis, get, set, options, save)
|
|
|
379
379
|
const change = (input)=>
|
|
380
380
|
{
|
|
381
381
|
const v = parseFloat(input.value);
|
|
382
|
-
if (!
|
|
382
|
+
if (!isFinite(v)) return; // a box part way through typing, or 1e999
|
|
383
383
|
set(v);
|
|
384
384
|
save();
|
|
385
385
|
};
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -510,14 +510,14 @@ function tweenProperty(target, propertyPath, start, end, duration = 1, options =
|
|
|
510
510
|
function tweenCarryOvershoot(tween)
|
|
511
511
|
{
|
|
512
512
|
const duration = tween.duration;
|
|
513
|
-
tween.life = duration ? min(tween.life, 0) % duration + duration : 1e-9;
|
|
513
|
+
tween.life = duration > 0 ? min(tween.life, 0) % duration + duration : 1e-9;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
// How many iterations the update that finished one ran through: that one and every whole one after it
|
|
517
517
|
function tweenPassed(tween)
|
|
518
518
|
{
|
|
519
519
|
const duration = tween.duration;
|
|
520
|
-
return duration ? 1 + floor(-min(tween.life, 0) / duration) : 1;
|
|
520
|
+
return duration > 0 ? 1 + floor(-min(tween.life, 0) / duration) : 1; // no time, or less, is one each update
|
|
521
521
|
}
|
|
522
522
|
|
|
523
523
|
// start the next iteration of a loop or pingPong, the time the last one ran over already spent, true
|
package/plugins/uiSystem.js
CHANGED
|
@@ -747,6 +747,8 @@ class UISystemPlugin
|
|
|
747
747
|
showConfirmDialog(text='Are you sure?', yesCallback, noCallback, size=vec2(500,250), exitKey='Escape')
|
|
748
748
|
{
|
|
749
749
|
ASSERT(!uiSystem.confirmDialog, 'a confirm dialog is already open, check uiSystem.confirmDialog');
|
|
750
|
+
if (uiSystem.confirmDialog)
|
|
751
|
+
return uiSystem.confirmDialog; // in a release build, the one open, as a second would break navigation
|
|
750
752
|
|
|
751
753
|
const savedNavigationDirection = uiSystem.navigationDirection;
|
|
752
754
|
const savedNavigationObject = uiSystem.navigationObject;
|
|
@@ -1412,7 +1414,7 @@ class UITextInput extends UIObject
|
|
|
1412
1414
|
onKeyDown(e)
|
|
1413
1415
|
{
|
|
1414
1416
|
// named keys by key, so numpad Enter works as Enter
|
|
1415
|
-
const code = e.code, key = e.key;
|
|
1417
|
+
const code = e.code, key = e.key || ''; // autofill sends a keydown with no key
|
|
1416
1418
|
if (e.repeat && (key === 'Enter' || code === 'Space'))
|
|
1417
1419
|
return; // a key held when editing began repeats, it should not type or stop editing
|
|
1418
1420
|
this.text += ''; // a game may have set a number
|
|
@@ -1646,12 +1648,14 @@ class UISlider extends UIObject
|
|
|
1646
1648
|
}
|
|
1647
1649
|
update()
|
|
1648
1650
|
{
|
|
1651
|
+
// held when the frame began, so a release in the same frame as the last move still reads it
|
|
1652
|
+
const wasActive = this.isActiveObject();
|
|
1649
1653
|
super.update();
|
|
1650
1654
|
if (!this.interactive)
|
|
1651
1655
|
return;
|
|
1652
1656
|
|
|
1653
1657
|
const oldValue = this.value;
|
|
1654
|
-
if (this.isActiveObject())
|
|
1658
|
+
if (wasActive || this.isActiveObject())
|
|
1655
1659
|
{
|
|
1656
1660
|
// handle horizontal or vertical slider
|
|
1657
1661
|
const isHorizontal = this.size.x > this.size.y;
|
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.24.
|
|
35
|
+
const engineVersion = '1.24.2';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -165,6 +165,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
|
|
|
165
165
|
let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
|
|
166
166
|
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
167
167
|
let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
|
|
168
|
+
let engineFrameErrors = 0; // errors a release build's loop went on past
|
|
168
169
|
|
|
169
170
|
// the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
|
|
170
171
|
// asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
|
|
@@ -450,7 +451,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
450
451
|
o.parent || o.updateTransforms();
|
|
451
452
|
|
|
452
453
|
// objects made and destroyed while paused, like a menu's effects, still leave the list
|
|
453
|
-
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
454
|
+
engineObjects.some(o=>o.destroyed) && (engineObjects = engineObjects.filter(o=>!o.destroyed));
|
|
454
455
|
}
|
|
455
456
|
else
|
|
456
457
|
engineObjectsUpdate();
|
|
@@ -497,7 +498,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
497
498
|
// render the game and objects
|
|
498
499
|
enginePreRender();
|
|
499
500
|
gameRender();
|
|
500
|
-
|
|
501
|
+
engineObjectsSort();
|
|
501
502
|
for (const o of engineObjects)
|
|
502
503
|
{
|
|
503
504
|
if (o.destroyed) continue;
|
|
@@ -600,7 +601,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
600
601
|
}
|
|
601
602
|
|
|
602
603
|
// the splash first, the images load under it, then the loading screen for the rest
|
|
603
|
-
showSplashScreen && await promises.
|
|
604
|
+
showSplashScreen && await promises[promises.length - 1];
|
|
604
605
|
return startEngine(promises);
|
|
605
606
|
|
|
606
607
|
// gameInit runs once the images are in, and the game loop starts once it and everything loaded while it ran are
|
|
@@ -723,7 +724,16 @@ function engineScheduleFrame()
|
|
|
723
724
|
const next = (frameTimeMS)=>
|
|
724
725
|
{
|
|
725
726
|
engineFrameScheduled = false;
|
|
726
|
-
|
|
727
|
+
if (engineManualStep) return;
|
|
728
|
+
if (debug) return engineUpdateInternal(frameTimeMS); // a debug build stops at an error, where it shows it
|
|
729
|
+
try { engineUpdateInternal(frameTimeMS); }
|
|
730
|
+
catch (error)
|
|
731
|
+
{
|
|
732
|
+
// a release build goes on past an error in a frame, a frozen game is the worst a player can get; only the
|
|
733
|
+
// first is logged, one every frame would flood the console
|
|
734
|
+
engineFrameErrors++ || console.error(error);
|
|
735
|
+
engineScheduleFrame();
|
|
736
|
+
}
|
|
727
737
|
};
|
|
728
738
|
if (typeof requestAnimationFrame === 'function')
|
|
729
739
|
requestAnimationFrame(next);
|
|
@@ -769,14 +779,18 @@ function engineObjectsUpdate()
|
|
|
769
779
|
++engineObjectsUpdateCount;
|
|
770
780
|
engineObjectsCollidePairs.clear();
|
|
771
781
|
// objects update in render order, which rendering keeps them in, so a headless run or a frame that rendered
|
|
772
|
-
// nothing updates them the same way
|
|
773
|
-
|
|
782
|
+
// nothing updates them the same way
|
|
783
|
+
engineObjectsSort();
|
|
774
784
|
// get list of solid objects for physics optimization, in update order, which 3D collision pairs by;
|
|
775
785
|
// 2D checks the static ones last, so a contact with a moving object can not leave something back inside a static
|
|
776
786
|
// solid it was already pushed out of
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
787
|
+
const fixed = [];
|
|
788
|
+
engineObjectsCollide = [];
|
|
789
|
+
engineObjectsCollideStaticLast = [];
|
|
790
|
+
for (const o of engineObjects)
|
|
791
|
+
o.collideSolidObjects && (engineObjectsCollide.push(o), (o.mass ? engineObjectsCollideStaticLast : fixed).push(o));
|
|
792
|
+
for (const o of fixed)
|
|
793
|
+
engineObjectsCollideStaticLast.push(o);
|
|
780
794
|
|
|
781
795
|
// update physics before object update
|
|
782
796
|
for (const o of engineObjects)
|
|
@@ -826,7 +840,16 @@ function engineObjectsUpdate()
|
|
|
826
840
|
updateTopObject(o);
|
|
827
841
|
|
|
828
842
|
// remove destroyed objects
|
|
829
|
-
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
843
|
+
engineObjects.some(o=>o.destroyed) && (engineObjects = engineObjects.filter(o=>!o.destroyed));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// sort the objects by render order, keeping the order of equals, only when one is out of order, as they are kept
|
|
847
|
+
// sorted and most frames change none
|
|
848
|
+
function engineObjectsSort()
|
|
849
|
+
{
|
|
850
|
+
for (let i = engineObjects.length; --i > 0;)
|
|
851
|
+
if (engineObjects[i].renderOrder < engineObjects[i-1].renderOrder)
|
|
852
|
+
return void engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
830
853
|
}
|
|
831
854
|
|
|
832
855
|
/** Destroy and remove all objects
|
package/src/engineAudio.js
CHANGED
|
@@ -58,9 +58,19 @@ function audioInit()
|
|
|
58
58
|
{
|
|
59
59
|
if (!soundEnable || headlessMode) return;
|
|
60
60
|
|
|
61
|
+
audioSetSession();
|
|
61
62
|
document.addEventListener('visibilitychange', audioVisibilityChange);
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// on an iPhone the silent switch mutes the page's sound, unless soundIgnoreSilentSwitch has it play as media,
|
|
66
|
+
// Safari 16.4 and up; elsewhere there is no audioSession and nothing changes
|
|
67
|
+
function audioSetSession()
|
|
68
|
+
{
|
|
69
|
+
const session = typeof navigator != 'undefined' && /** @type {any} */ (navigator).audioSession;
|
|
70
|
+
if (session)
|
|
71
|
+
session.type = soundIgnoreSilentSwitch ? 'playback' : 'auto';
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
// a hidden page stops the game, so its sound stops too, and the audio clock with it so every sound picks up
|
|
65
75
|
// exactly where it was; only a suspend made here is undone, not one the browser holds until the first input
|
|
66
76
|
let audioSuspendedWhenHidden = false;
|
|
@@ -203,13 +213,13 @@ function audioParamRamp(param, value, fadeTime=0)
|
|
|
203
213
|
* @memberof Audio
|
|
204
214
|
* @example
|
|
205
215
|
* // load an audio asset file
|
|
206
|
-
* const
|
|
216
|
+
* const music = new Sound('sound.mp3');
|
|
207
217
|
*
|
|
208
218
|
* // create a zzfx sound
|
|
209
|
-
* const
|
|
219
|
+
* const blip = new Sound([.5,.5]);
|
|
210
220
|
*
|
|
211
221
|
* // play a sound
|
|
212
|
-
*
|
|
222
|
+
* blip.play();
|
|
213
223
|
*/
|
|
214
224
|
class Sound
|
|
215
225
|
{
|
package/src/engineDebug.js
CHANGED
|
@@ -269,7 +269,10 @@ function debugShowErrors()
|
|
|
269
269
|
{
|
|
270
270
|
// replace entire page with error message
|
|
271
271
|
document.body.style.cssText = 'background-color:#111;margin:8px';
|
|
272
|
-
|
|
272
|
+
const pre = document.createElement('pre'); // the message as text, a file name in it never markup
|
|
273
|
+
pre.style.cssText = 'color:#f00;font-size:28px;white-space:pre-wrap';
|
|
274
|
+
pre.textContent = String(message);
|
|
275
|
+
document.body.replaceChildren(pre);
|
|
273
276
|
}
|
|
274
277
|
|
|
275
278
|
const originalAssert = console.assert;
|
package/src/engineDraw.js
CHANGED
|
@@ -182,6 +182,10 @@ class TileInfo
|
|
|
182
182
|
this.padding = padding;
|
|
183
183
|
/** @property {TextureInfo} - The texture info for this tile */
|
|
184
184
|
this.textureInfo = textureInfo;
|
|
185
|
+
/** @property {Array<TileInfo>|undefined} - A tile set's tiles, each wherever it was packed, as loadTiles makes
|
|
186
|
+
* them: a tile layer given this tile info draws its tile n from tiles[n] and not from a grid on one sheet
|
|
187
|
+
* @type {Array<TileInfo>|undefined} */
|
|
188
|
+
this.tiles = undefined;
|
|
185
189
|
/** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
|
|
186
190
|
this.bleed = bleed;
|
|
187
191
|
/** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
|
|
@@ -868,13 +872,15 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
|
|
|
868
872
|
if (useWebGL && glEnable)
|
|
869
873
|
{
|
|
870
874
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
871
|
-
let
|
|
875
|
+
let sx = 1, sy = 1;
|
|
872
876
|
if (screenSpace)
|
|
873
877
|
{
|
|
874
878
|
if (glSkipScreenSpace) return;
|
|
875
|
-
|
|
879
|
+
let size;
|
|
880
|
+
[pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
|
|
881
|
+
sx = size.x, sy = size.y;
|
|
876
882
|
}
|
|
877
|
-
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y,
|
|
883
|
+
glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, sx, sy, angle, wrap);
|
|
878
884
|
}
|
|
879
885
|
else
|
|
880
886
|
{
|
|
@@ -940,17 +946,22 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
940
946
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
941
947
|
ASSERT(isNumber(sides), 'sides must be a number');
|
|
942
948
|
|
|
943
|
-
// build regular polygon points
|
|
944
|
-
const points =
|
|
949
|
+
// build regular polygon points, into vectors kept for the next, as drawPoly is done with them when it returns
|
|
950
|
+
const points = drawRegularPolyPoints, pool = drawRegularPolyPool;
|
|
951
|
+
points.length = 0;
|
|
945
952
|
const sizeX = size.x/2, sizeY = size.y/2;
|
|
946
953
|
for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
|
|
947
954
|
{
|
|
948
|
-
const a = (i/sides)*PI*2;
|
|
949
|
-
|
|
955
|
+
const a = (i/sides)*PI*2, point = pool[points.length] ||= vec2();
|
|
956
|
+
point.x = sin(a)*sizeX, point.y = cos(a)*sizeY;
|
|
957
|
+
points.push(point);
|
|
950
958
|
}
|
|
951
959
|
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
952
960
|
}
|
|
953
961
|
|
|
962
|
+
// the points drawRegularPoly draws and the vectors it writes them into
|
|
963
|
+
const drawRegularPolyPoints = [], drawRegularPolyPool = [];
|
|
964
|
+
|
|
954
965
|
/** Draw colored polygon using passed in points
|
|
955
966
|
* - WebGL fills a polygon whose edges do not cross, concave or not; a self crossing one, like a star through its
|
|
956
967
|
* outer points, fills wrong there, so draw it as its simple outline or in parts
|
|
@@ -978,15 +989,17 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
978
989
|
if (useWebGL && glEnable)
|
|
979
990
|
{
|
|
980
991
|
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
981
|
-
let
|
|
992
|
+
let sx = 1, sy = 1;
|
|
982
993
|
if (screenSpace)
|
|
983
994
|
{
|
|
984
995
|
if (glSkipScreenSpace) return;
|
|
985
|
-
|
|
996
|
+
let size;
|
|
997
|
+
[pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
|
|
998
|
+
sx = size.x, sy = size.y;
|
|
986
999
|
}
|
|
987
|
-
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y,
|
|
1000
|
+
glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, sx, sy, angle);
|
|
988
1001
|
if (lineWidth > 0)
|
|
989
|
-
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y,
|
|
1002
|
+
glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, sx, sy, angle);
|
|
990
1003
|
}
|
|
991
1004
|
else
|
|
992
1005
|
{
|
|
@@ -1631,7 +1644,7 @@ function tintImageData(data, color, additiveColor)
|
|
|
1631
1644
|
if (additiveColor && !isBlack(additiveColor))
|
|
1632
1645
|
{
|
|
1633
1646
|
// multiply + additive (slower), color.a is baked into the alpha channel here
|
|
1634
|
-
const colorMultiply = [color.r, color.g, color.b, color.a];
|
|
1647
|
+
const colorMultiply = [clamp(color.r), clamp(color.g), clamp(color.b), clamp(color.a)];
|
|
1635
1648
|
const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
|
|
1636
1649
|
additiveColor.b * 255, additiveColor.a * 255];
|
|
1637
1650
|
for (let i = 0; i < data.length; ++i)
|
|
@@ -1642,9 +1655,9 @@ function tintImageData(data, color, additiveColor)
|
|
|
1642
1655
|
// RGB only, faster — alpha left intact for the caller
|
|
1643
1656
|
for (let i = 0; i < data.length; i+=4)
|
|
1644
1657
|
{
|
|
1645
|
-
data[i ] *= color.r;
|
|
1646
|
-
data[i+1] *= color.g;
|
|
1647
|
-
data[i+2] *= color.b;
|
|
1658
|
+
data[i ] *= clamp(color.r);
|
|
1659
|
+
data[i+1] *= clamp(color.g);
|
|
1660
|
+
data[i+2] *= clamp(color.b);
|
|
1648
1661
|
}
|
|
1649
1662
|
return false;
|
|
1650
1663
|
}
|
|
@@ -1896,7 +1909,7 @@ function engineGlowTexture(falloff=1)
|
|
|
1896
1909
|
ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
|
|
1897
1910
|
const key = max(round(falloff * 10), 1) / 10;
|
|
1898
1911
|
let texture = engineGlowTextures.get(key);
|
|
1899
|
-
if (texture || !glContext ||
|
|
1912
|
+
if (texture || !glContext || !canvasAvailable()) return texture;
|
|
1900
1913
|
const size = 64, context = createCanvasContext(size), steps = 16;
|
|
1901
1914
|
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
|
1902
1915
|
for (let i = 0; i <= steps; ++i)
|