littlejsengine 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,19 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
73
73
  * - What a file written by hand gets wrong uses the default: a value that is not of its default's type
74
74
  * - An object its type can not make is skipped with an error in debug builds, where asserts throw, and the rest
75
75
  * of the level is made
76
+ * - A level may hold a map of blocks, in a voxels block: pos, its corner, size, its cells along x, y and z, and
77
+ * blocks, runs of a count and a type along x, then y, then z; it is made a VoxelMap, the first of what is
78
+ * returned, see level3DVoxelSetup for its sheet
79
+ * - A level may hold a terrain, in a terrain block: pos, its center, size, its size in the world along x and z,
80
+ * height, how tall a full height is, color, and heights, rows of 0 to 1 from -z to +z, each from -x to +x; it
81
+ * is made a HeightMap, returned with what else was made; paint, when it has it, colors its samples: colors, a
82
+ * list, and cells, runs of a count and a color along the rows, 0 for the terrain's own and 1 the list's first
83
+ * - A level may hold prefabs of its own, in a prefabs block, each by its name as level3DAddPrefab takes it; they
84
+ * are added before its objects are made, one the game added itself keeps its place
85
+ * - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
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 shadows; what the block
88
+ * leaves out stays as the game set it, and a level with no block changes nothing
76
89
  * @param {Object} level - The level, the level editor edits this same object
77
90
  * @return {Array<any>} - What each object's type made, a function that made nothing is left out
78
91
  * @memberof Level3D */
@@ -81,7 +94,12 @@ function level3DLoad(level)
81
94
  ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
82
95
  ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
83
96
  editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
84
- const made = [];
97
+ level3DPrefabsAdd(level.prefabs);
98
+ level3DSceneApply(level.scene);
99
+ const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
100
+ map && made.push(map);
101
+ const terrain = level3DTerrainMap = level3DTerrainMake(level.terrain);
102
+ terrain && made.push(terrain);
85
103
  for (const object of isArray(level.objects) ? level.objects : [])
86
104
  {
87
105
  if (!object || typeof object !== 'object') continue;
@@ -92,6 +110,182 @@ function level3DLoad(level)
92
110
  return made;
93
111
  }
94
112
 
113
+ // the sheet a level's block map is made with, and what sets each one up, see level3DVoxelSetup; and the map the
114
+ // level loaded last made, the one the 3D editor paints
115
+ let level3DVoxelTiles, level3DVoxelSetupMap, level3DVoxelMap;
116
+
117
+ /** How a level's block map is made: the sheet its blocks show tiles of, and a function to set it up
118
+ * - A level's voxels block makes a VoxelMap when the level loads, with texture 0 and the default tile size unless
119
+ * a sheet is given here; a block's type shows that tile of the sheet on every face
120
+ * - setup is called with each map a level makes, to give block types their own faces or make them see-through
121
+ * - Call it before level3DLoad; with no arguments the defaults are back
122
+ * @param {TileInfo} [tileInfo] - The sheet's first tile, as for a VoxelMap
123
+ * @param {function(VoxelMap): void} [setup]
124
+ * @example
125
+ * level3DVoxelSetup(tile(0, 16, 1), (map)=> map.setBlockType(1, {top: 0, side: 1, bottom: 2})); // grass
126
+ * @memberof Level3D */
127
+ function level3DVoxelSetup(tileInfo, setup)
128
+ {
129
+ level3DVoxelTiles = tileInfo;
130
+ level3DVoxelSetupMap = setup;
131
+ }
132
+
133
+ // the most cells a level's map may have, 256 each way
134
+ const LEVEL3D_VOXEL_CELLS = 2 ** 24;
135
+
136
+ // a map's blocks as the file keeps them, runs of a count and a type, cell by cell in the map's own order
137
+ function level3DVoxelsEncode(data)
138
+ {
139
+ const blocks = [];
140
+ for (let i = 0; i < data.length;)
141
+ {
142
+ let end = i + 1;
143
+ while (end < data.length && data[end] === data[i]) ++end;
144
+ blocks.push(end - i, data[i]);
145
+ i = end;
146
+ }
147
+ return blocks;
148
+ }
149
+
150
+ // runs into a map's cells: what they do not reach is empty, a type that is not 0 to 255 is empty, and a run that
151
+ // is not two numbers ends them
152
+ function level3DVoxelsDecode(blocks, data)
153
+ {
154
+ data.fill(0);
155
+ if (!isArray(blocks)) return;
156
+ for (let i = 0, cell = 0; i + 1 < blocks.length && cell < data.length; i += 2)
157
+ {
158
+ const count = blocks[i], type = blocks[i + 1];
159
+ if (!isNumber(count) || !isNumber(type) || count < 0) return;
160
+ const end = min(cell + floor(count), data.length);
161
+ data.fill(type >= 0 && type <= 255 && type % 1 === 0 ? type : 0, cell, end);
162
+ cell = end;
163
+ }
164
+ }
165
+
166
+ // the corner and the size in cells of a level's voxels block, undefined when it is not one a map can be made of
167
+ function level3DVoxelsShape(voxels)
168
+ {
169
+ if (!voxels || typeof voxels !== 'object') return;
170
+ const size = level3DVector(voxels.size);
171
+ if (!size || [size.x, size.y, size.z].some((n)=> n < 1 || n % 1) ||
172
+ size.x * size.y * size.z > LEVEL3D_VOXEL_CELLS) return;
173
+ return {pos: level3DVector(voxels.pos, vec3()), size};
174
+ }
175
+
176
+ // the VoxelMap of a level's voxels block, undefined when it has none or it is wrong
177
+ function level3DVoxelsMake(voxels)
178
+ {
179
+ const shape = level3DVoxelsShape(voxels);
180
+ if (!shape || typeof VoxelMap == 'undefined') return;
181
+ const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles || tile());
182
+ level3DVoxelsDecode(voxels.blocks, map.data);
183
+ level3DVoxelSetupMap?.(map);
184
+ map.rebuild();
185
+ return map;
186
+ }
187
+
188
+ // the terrain the level loaded last made, the one the 3D editor sculpts, and the most samples it may have a side
189
+ let level3DTerrainMap;
190
+ const LEVEL3D_TERRAIN_SAMPLES = 129;
191
+
192
+ // what a level's terrain block is made with: its center, its size in the world along x and z, how tall a full
193
+ // height is, and its heights as rows of 0 to 1, a copy, each row as long as the first; undefined when it is not
194
+ // one a HeightMap can be made of
195
+ function level3DTerrainShape(terrain)
196
+ {
197
+ if (!terrain || typeof terrain !== 'object') return;
198
+ const size = terrain.size, rows = terrain.heights, n = LEVEL3D_TERRAIN_SAMPLES;
199
+ if (!isArray(size) || size.length !== 2 || !size.every((v)=> isNumber(v) && v > 0)) return;
200
+ if (!isArray(rows) || rows.length < 2 || rows.length > n || !isArray(rows[0])) return;
201
+ const columns = rows[0].length;
202
+ if (columns < 2 || columns > n || !rows.every((row)=> isArray(row) && row.length === columns)) return;
203
+ const heights = rows.map((row)=> row.map((v)=> isNumber(v) ? clamp(v) : 0));
204
+ return {pos: level3DVector(terrain.pos, vec3()), size: vec2(size[0], size[1]),
205
+ height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
206
+ }
207
+
208
+ // a hex color of a level as a Color, undefined when it is not one
209
+ function level3DHexColor(hex)
210
+ { return /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex) ? new Color().setHex(hex) : undefined; }
211
+
212
+ // the paint of a level's terrain block, for a terrain of so many samples: its colors as they are written and a
213
+ // color for each sample, row after row, 0 for none and 1 the first of the colors; undefined when it has none or
214
+ // it does not fit the terrain
215
+ function level3DTerrainPaint(terrain, samples)
216
+ {
217
+ const paint = terrain?.paint, colors = paint?.colors, runs = paint?.cells;
218
+ if (!isArray(colors) || !isArray(runs) || colors.length > 255 || !colors.every(level3DHexColor)) return;
219
+ const cells = new Uint8Array(samples);
220
+ let at = 0;
221
+ for (let i = 0; i < runs.length; i += 2)
222
+ {
223
+ const count = runs[i], color = runs[i + 1];
224
+ if (!(count > 0 && count % 1 === 0 && color >= 0 && color % 1 === 0) || color > colors.length ||
225
+ at + count > samples) return;
226
+ cells.fill(color, at, at += count);
227
+ }
228
+ return at === samples ? {colors: colors.slice(), cells} : undefined;
229
+ }
230
+
231
+ // give a terrain's map the colors of its block: its own color over the whole of it, or with paint a color for
232
+ // each sample, the terrain's own where it is not painted; the caller builds the map again
233
+ function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain, map.rows * map.columns))
234
+ {
235
+ const base = level3DHexColor(terrain.color) || WHITE;
236
+ map.color = base, map.colors = undefined;
237
+ if (!paint || !paint.cells.some((v)=> v)) return;
238
+ const palette = [base, ...paint.colors.map(level3DHexColor)];
239
+ map.color = WHITE;
240
+ map.colors = map.heights.map((row, r)=> row.map((v, c)=> palette[paint.cells[r * map.columns + c]] || base));
241
+ }
242
+
243
+ // the HeightMap of a level's terrain block, undefined when it has none or it is wrong
244
+ function level3DTerrainMake(terrain)
245
+ {
246
+ const shape = level3DTerrainShape(terrain);
247
+ if (!shape || typeof HeightMap == 'undefined') return;
248
+ const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
249
+ level3DTerrainSetColors(map, terrain);
250
+ map.colors && map.rebuild();
251
+ return map;
252
+ }
253
+
254
+ // the sky domes levels made, a level's sky takes the place of the one before and disposes it; a dome the game made
255
+ // itself is left whole, for the game to put back
256
+ const level3DSkies = new WeakSet;
257
+
258
+ // set the scene a level's scene block gives: sky as its top, horizon and bottom colors, with ambient how much of
259
+ // them lights the scene, sunDirection and sunColor, fog as its start and end with fogColor, the horizon without one,
260
+ // and shadows; a setting the block does not have, or has wrong, stays as it is
261
+ function level3DSceneApply(scene)
262
+ {
263
+ const r = render3D;
264
+ if (!r || !scene || typeof scene !== 'object') return;
265
+ const color = (value)=> /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) ? new Color().setHex(value) : undefined;
266
+ const sky = isArray(scene.sky) && scene.sky.length === 3 ? scene.sky.map(color) : [];
267
+ if (sky.length && sky.every((c)=> c))
268
+ {
269
+ // as setSky does, without disposing a dome that is the game's own
270
+ const [top, horizon, bottom] = sky, ambient = isNumber(scene.ambient) ? scene.ambient : .5;
271
+ level3DSkies.has(r.sky) && r.sky.dispose();
272
+ level3DSkies.add(r.sky = buildSky(top, horizon, bottom));
273
+ r.fogColor = horizon.copy();
274
+ r.ambientColor = top.scale(ambient, 1);
275
+ r.ambientGroundColor = bottom.scale(ambient, 1);
276
+ }
277
+ const sun = level3DVector(scene.sunDirection);
278
+ if (sun && sun.lengthSquared())
279
+ r.sunDirection = sun;
280
+ r.sunColor = color(scene.sunColor) || r.sunColor;
281
+ const fog = scene.fog;
282
+ if (isArray(fog) && fog.length === 2 && fog.every((v)=> isNumber(v)))
283
+ r.fogStart = fog[0], r.fogEnd = fog[1];
284
+ r.fogColor = color(scene.fogColor) || r.fogColor;
285
+ if (typeof scene.shadows === 'boolean')
286
+ r.shadows = scene.shadows;
287
+ }
288
+
95
289
  // a vec3 of an array of three numbers, as the file has them, or the fallback
96
290
  function level3DVector(value, fallback)
97
291
  {
@@ -126,6 +320,14 @@ function level3DProperties(type, object)
126
320
  // make one object of a level from the type added for its name, undefined when there is no such type or it made
127
321
  // nothing
128
322
  function level3DMake(object)
323
+ {
324
+ return level3DMakeAt(object, level3DVector(object.pos, vec3()),
325
+ level3DVector(object.rotation, vec3()).scale(PI / 180), level3DVector(object.scale, vec3(1)));
326
+ }
327
+
328
+ // make a level object's game object at a place, a rotation in radians and a scale, its own as level3DMake gives
329
+ // them or the ones a prefab's instance puts it at
330
+ function level3DMakeAt(object, pos, rotation, scale)
129
331
  {
130
332
  const type = level3DTypes.get(object.type);
131
333
  if (!type)
@@ -133,7 +335,7 @@ function level3DMake(object)
133
335
  debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
134
336
  return;
135
337
  }
136
- const pos = level3DVector(object.pos, vec3()), properties = level3DProperties(type, object);
338
+ const properties = level3DProperties(type, object);
137
339
  const {make} = type, count = engineObjects.length;
138
340
  let result;
139
341
  try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
@@ -151,16 +353,263 @@ function level3DMake(object)
151
353
  if (result instanceof EngineObject3D)
152
354
  {
153
355
  level3DBaseScale.set(result, result.scale3D.copy());
154
- result.rotation3D = level3DVector(object.rotation, vec3()).scale(PI / 180);
155
- result.scale3D = result.scale3D.multiply(level3DVector(object.scale, vec3(1)));
356
+ result.rotation3D = rotation.copy();
357
+ result.scale3D = result.scale3D.multiply(scale);
156
358
  }
157
359
 
158
360
  // the type's own properties are set on it, one the type has no default for could be a field of the engine's
159
361
  for (const key in type.defaults)
160
362
  result[key] = properties[key];
363
+ // a prefab's instance makes its parts now that it is where it goes
364
+ result instanceof Prefab3D && result.placeAt(pos);
161
365
  return result;
162
366
  }
163
367
 
368
+ /** Make one object of a type added with level3DAddType, level3DAddMesh or level3DAddPrefab, from code, with no
369
+ * level: a prefab's instance, or a plain type
370
+ * @param {string} type - The type's name
371
+ * @param {Vector3} [pos3D]
372
+ * @param {Vector3} [rotation3D] - In radians, as an object has it
373
+ * @param {Vector3} [scale3D] - Times the scale the type makes it with
374
+ * @param {Object} [properties] - Over the type's defaults
375
+ * @return {any} - What the type made, a Prefab3D for a prefab, undefined when there is no such type
376
+ * @memberof Level3D
377
+ * @example
378
+ * level3DAddPrefab('House', await fetchJSON('house.json'));
379
+ * level3DSpawn('House', vec3(10, 0, 0), vec3(0, PI/2, 0)); */
380
+ function level3DSpawn(type, pos3D=vec3(), rotation3D=vec3(), scale3D=vec3(1), properties)
381
+ {
382
+ ASSERT(isVector3(pos3D) && isVector3(rotation3D) && isVector3(scale3D), 'pos3D, rotation3D and scale3D are vec3');
383
+ return level3DMakeAt(properties ? {type, properties} : {type}, pos3D.copy(), rotation3D, scale3D);
384
+ }
385
+
386
+ ///////////////////////////////////////////////////////////////////////////////
387
+ // prefabs: a prefab is a small level, a list of objects about its own origin, placed many times
388
+
389
+ // the prefabs by name: how an instance is built, its objects, fromLevel, true for one a level's own prefabs
390
+ // block added, which one the game adds takes the place of, and source, the object it was added from, which the
391
+ // level editor knows a level that is a prefab by
392
+ /** @type {Map<string, {attached: boolean, objects: Array<Object>, fromLevel: boolean, source: Object}>} */
393
+ const level3DPrefabs = new Map;
394
+
395
+ // how many prefabs deep an instance being made is, a prefab that holds itself is stopped; and how many of those
396
+ // are attached: a prefab inside an attached one is attached too, or its parts would be left behind in the world
397
+ let level3DPrefabDepth = 0, level3DPrefabAttached = 0;
398
+
399
+ /** Add a prefab: a small level, its objects placed about its own origin, to place many times under one name
400
+ * - It is a type from then on: a level's object of that type, the level editor's Place list and level3DSpawn make
401
+ * an instance, a Prefab3D; an instance is made of the prefab as it is then, so the level editor, where a prefab
402
+ * changes, makes its instances again
403
+ * - The prefab is a level as the level editor saves it, {objects: [...]}, so the editor is the prefab editor too;
404
+ * only its objects are used, and they may be of other prefabs
405
+ * - With attached true in it the parts are children of the instance and move with it as one body, without
406
+ * collision of their own, a prefab inside it too; its handle is at the middle of the box around them and its
407
+ * size3D is that box, so setCollision makes the body solid where it is seen; otherwise each part is an object of its own in the world and collides as one placed
408
+ * by hand does
409
+ * - Adding a name again replaces it
410
+ * @param {string} name - The type its instances have in a level
411
+ * @param {Object} prefab - {objects, attached}
412
+ * @memberof Level3D
413
+ * @example
414
+ * level3DAddPrefab('Tower', {objects: [{type: 'Box', pos: [0, 1, 0], scale: [2, 2, 2]},
415
+ * {type: 'Cylinder', pos: [0, 3, 0]}]});
416
+ * level3DLoad({objects: [{type: 'Tower', pos: [5, 0, 5]}, {type: 'Tower', pos: [-5, 0, 5], rotation: [0, 45, 0]}]}); */
417
+ function level3DAddPrefab(name, prefab) { level3DPrefabSet(name, prefab, false); }
418
+
419
+ // add a prefab, the game's or a level's own
420
+ function level3DPrefabSet(name, prefab, fromLevel)
421
+ {
422
+ ASSERT(!!prefab && typeof prefab === 'object', 'a prefab is an object, {objects: [...]}');
423
+ name = String(name);
424
+ const objects = isArray(prefab?.objects) ? prefab.objects.filter((o)=> o && typeof o === 'object') : [];
425
+ // a copy, what the game or the editor does to its own afterwards does not change the instances made later
426
+ level3DPrefabs.set(name, {attached: !!prefab?.attached, objects: JSON.parse(JSON.stringify(objects)), fromLevel,
427
+ source: prefab});
428
+ // a class of its own, so the level editor makes its instances again as it does a class's
429
+ level3DTypes.set(name, {make: class extends Prefab3D { constructor(pos) { super(pos, name); } }, defaults: {},
430
+ tileInfo: undefined});
431
+ }
432
+
433
+ // add a level's own prefabs, each unless the game added one of that name or it is a plain type's name
434
+ function level3DPrefabsAdd(prefabs)
435
+ {
436
+ if (!prefabs || typeof prefabs !== 'object' || isArray(prefabs)) return;
437
+ for (const name in prefabs)
438
+ {
439
+ const known = level3DPrefabs.get(name);
440
+ if (known ? !known.fromLevel : level3DTypes.has(name)) continue;
441
+ prefabs[name] && typeof prefabs[name] === 'object' && level3DPrefabSet(name, prefabs[name], true);
442
+ }
443
+ }
444
+
445
+ /** Load a prefab from a file the level editor saved and add it
446
+ * @param {string} name - The type its instances have in a level
447
+ * @param {string} url
448
+ * @return {Promise<void>}
449
+ * @memberof Level3D */
450
+ async function level3DLoadPrefab(name, url) { level3DAddPrefab(name, await fetchJSON(url)); }
451
+
452
+ // where an instance puts one of its prefab's objects: the object's own place, turn and size inside the instance's
453
+ function level3DPrefabPartTransform(instance, object)
454
+ {
455
+ const pos = level3DVector(object.pos, vec3()), scale = level3DVector(object.scale, vec3(1));
456
+ const rotation = level3DVector(object.rotation, vec3()).scale(PI / 180);
457
+ // the two turns one after the other; the sizes multiply along each axis, which is exact for an instance sized
458
+ // evenly or a part that is not turned, and as near as a box can be otherwise
459
+ const turn = buildMatrix(vec3(), instance.rotation3D).multiply(buildMatrix(vec3(), rotation));
460
+ return {pos: buildMatrix(instance.pos3D, instance.rotation3D, instance.scale3D).transformPoint(pos),
461
+ rotation: turn.getRotation(), scale: scale.multiply(instance.scale3D)};
462
+ }
463
+
464
+ /**
465
+ * An instance of a prefab, what a prefab's type makes: a handle with no shape of its own, and its parts, what the
466
+ * prefab's objects made
467
+ * - Parts of a prefab that is not attached are objects of their own in the world: moving, turning or sizing the
468
+ * handle puts them where it now says with its next update, or call placeParts; destroying it destroys them
469
+ * - Parts of an attached prefab are its children and move with it as one body
470
+ * @extends EngineObject3D
471
+ * @memberof Level3D
472
+ */
473
+ class Prefab3D extends EngineObject3D
474
+ {
475
+ /** Create an instance of a prefab, made by its type: place one with a level or level3DSpawn
476
+ * @param {Vector3} [pos3D]
477
+ * @param {string} [prefabName] - A prefab added with level3DAddPrefab */
478
+ constructor(pos3D, prefabName='')
479
+ {
480
+ super(pos3D);
481
+ /** @property {string} - The prefab it is an instance of */
482
+ this.prefabName = prefabName;
483
+ /** @property {boolean} - Are its parts its children, moving with it as one body */
484
+ this.attached = !!level3DPrefabs.get(prefabName)?.attached || level3DPrefabAttached > 0;
485
+ /** @property {Array<any>} - What the prefab's objects made, in the prefab's order */
486
+ this.parts = [];
487
+ /** @property {Vector3} - From the prefab's origin to the handle, in the prefab's own space: nothing for
488
+ * separate parts, and for an attached prefab the middle of the box around its parts, where its handle
489
+ * is, as an object's place is the middle of its body */
490
+ this.originOffset = vec3();
491
+ this.partObjects = []; // the prefab's object of each part
492
+ this.partsPlaced = ''; // where the handle was when its parts were last placed, undefined parts not made
493
+ this.partsMade = false;
494
+ }
495
+
496
+ /** Put the instance with its prefab's origin at a place, turned and sized as the handle is: where a level's
497
+ * object or level3DSpawn says it goes; the handle of an attached prefab is then at the middle of its body
498
+ * @param {Vector3} pos3D */
499
+ placeAt(pos3D)
500
+ {
501
+ this.pos3D = pos3D.copy();
502
+ this.placeParts();
503
+ const o = this.originOffset;
504
+ if (o.x || o.y || o.z)
505
+ this.pos3D = buildMatrix(pos3D, this.rotation3D, this.scale3D).transformPoint(o);
506
+ }
507
+
508
+ /** Put the parts where the handle is now, making them the first time; called by the handle's update when it
509
+ * has moved, turned or changed size */
510
+ placeParts()
511
+ {
512
+ if (this.destroyed) return;
513
+ this.partsMade ||= (this.makeParts(), true);
514
+ this.partsPlaced = this.placeKey();
515
+ if (this.attached) return; // children follow by themselves
516
+ this.parts.forEach((part, i)=>
517
+ {
518
+ if (!(part instanceof EngineObject3D)) return; // what a function made is where it was made
519
+ const to = level3DPrefabPartTransform(this, this.partObjects[i]);
520
+ part.rotation3D = to.rotation;
521
+ part.scale3D = (level3DBaseScale.get(part) ?? vec3(1)).multiply(to.scale);
522
+ // a prefab inside this one goes by its origin, which is not where an attached one's handle is
523
+ part instanceof Prefab3D ? part.placeAt(to.pos) : part.pos3D = to.pos;
524
+ });
525
+ }
526
+
527
+ // the handle's place, turn and size as one text, to tell when it has changed
528
+ placeKey()
529
+ {
530
+ const p = this.pos3D, r = this.rotation3D, s = this.scale3D;
531
+ return [p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z].join();
532
+ }
533
+
534
+ // make the parts, each where the instance puts it, or attached at its own place inside the instance
535
+ makeParts()
536
+ {
537
+ const prefab = level3DPrefabs.get(this.prefabName);
538
+ if (!prefab) return;
539
+ if (level3DPrefabDepth >= 8)
540
+ {
541
+ debug && console.error(`level3DLoad: the prefab ${this.prefabName} holds itself, left out there`);
542
+ return;
543
+ }
544
+ ++level3DPrefabDepth;
545
+ this.attached && ++level3DPrefabAttached;
546
+ try
547
+ {
548
+ const low = vec3(Infinity), high = vec3(-Infinity), children = [];
549
+ for (const object of prefab.objects)
550
+ {
551
+ const local = {pos: level3DVector(object.pos, vec3()), scale: level3DVector(object.scale, vec3(1)),
552
+ rotation: level3DVector(object.rotation, vec3()).scale(PI / 180)};
553
+ const at = this.attached ? local : level3DPrefabPartTransform(this, object);
554
+ const part = level3DMakeAt(object, at.pos, at.rotation, at.scale);
555
+ if (!part) continue; // level3DMakeAt gives an object or nothing
556
+ this.parts.push(part);
557
+ this.partObjects.push(object);
558
+ if (!this.attached || !(part instanceof EngineObject3D)) continue;
559
+ // a child rides with its parent and has no collision of its own
560
+ part.setCollision(false, false, false);
561
+ children.push(part);
562
+ // the corners of its box as it is turned; its place is its middle, a prefab inside this one too
563
+ const box = buildMatrix(part.pos3D, part.rotation3D, part.size3D.multiply(part.scale3D));
564
+ for (let i = 8; i--;)
565
+ {
566
+ const p = box.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
567
+ for (const k of ['x', 'y', 'z'])
568
+ low[k] = min(low[k], p[k]), high[k] = max(high[k], p[k]);
569
+ }
570
+ }
571
+ // an attached instance is the box around its parts, its handle at the middle of it as an object's
572
+ // place is the middle of its body, so a game that makes it solid has it solid where it is seen
573
+ if (children.length)
574
+ {
575
+ this.size3D = high.subtract(low);
576
+ this.originOffset = low.add(high).scale(.5);
577
+ }
578
+ for (const part of children)
579
+ {
580
+ part.pos3D = part.pos3D.subtract(this.originOffset);
581
+ this.addChild(part);
582
+ }
583
+ }
584
+ finally
585
+ {
586
+ --level3DPrefabDepth;
587
+ this.attached && --level3DPrefabAttached;
588
+ }
589
+ }
590
+
591
+ /** Keep the parts with the handle, called automatically each frame */
592
+ update()
593
+ {
594
+ super.update();
595
+ if (!this.attached && this.partsPlaced !== this.placeKey())
596
+ this.placeParts();
597
+ }
598
+
599
+ /** Destroy the instance and its parts
600
+ * @param {boolean} [immediate] */
601
+ destroy(immediate)
602
+ {
603
+ if (this.destroyed) return;
604
+ for (const part of this.parts)
605
+ part.parent === this || part.destroy?.(immediate); // children go with their parent
606
+ super.destroy(immediate);
607
+ }
608
+
609
+ /** A prefab's instance has nothing of its own to draw */
610
+ render3D() {}
611
+ }
612
+
164
613
  ///////////////////////////////////////////////////////////////////////////////
165
614
  // the built-in types
166
615
 
@@ -184,6 +633,14 @@ level3DAddType('Sphere', function(pos, properties)
184
633
  level3DAddType('Cylinder', function(pos, properties)
185
634
  { return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
186
635
  {color: WHITE, tile: -1, solid: true});
636
+ // a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
637
+ // object's rotation aims it, softness is how much of the cone fades, and shadows makes it the one that casts them
187
638
  level3DAddType('Light', function(pos, properties)
188
- { return new Light3D(pos, properties.radius, properties.color, properties.intensity); },
189
- {color: WHITE, radius: 5, intensity: 1});
639
+ {
640
+ const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
641
+ light.coneAngle = max(properties.cone, 0) * PI / 180;
642
+ light.coneSoftness = properties.softness;
643
+ if (properties.shadows && render3D)
644
+ render3D.shadowLight = light;
645
+ return light;
646
+ }, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
@@ -38,9 +38,10 @@ function setScene(scene)
38
38
 
39
39
  if (sceneCurrent)
40
40
  {
41
+ // a leave that throws still lets the next switch through
41
42
  sceneLeaving = true;
42
- sceneCurrent.leave?.();
43
- sceneLeaving = false;
43
+ try { sceneCurrent.leave?.(); }
44
+ finally { sceneLeaving = false; }
44
45
  }
45
46
  engineObjectsDestroy();
46
47
  sceneCurrent = scene;
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.21.0';
35
+ const engineVersion = '1.23.0';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -220,9 +220,12 @@ class Sound
220
220
  * @param {number} [taper=soundDefaultTaper] - At what percentage of range should it start tapering
221
221
  * @param {SoundLoadCallback} [onloadCallback] - callback function to call when sound is loaded
222
222
  */
223
- constructor(asset, randomness, range=soundDefaultRange, taper=soundDefaultTaper, onloadCallback)
223
+ constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
224
224
  {
225
225
  if (!soundEnable || headlessMode) return;
226
+ const rangeIsDefault = range === undefined;
227
+ if (rangeIsDefault)
228
+ range = soundDefaultRange;
226
229
 
227
230
  ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
228
231
  ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
@@ -230,8 +233,10 @@ class Sound
230
233
  ASSERT(isNumber(range), 'range must be a number');
231
234
  ASSERT(isNumber(taper), 'taper must be a number');
232
235
 
233
- /** @property {number} - World space max range of sound */
234
236
  this.range = range;
237
+ /** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
238
+ * to render3D.soundDefaultRange instead, which is further; setting range makes it false */
239
+ this.rangeIsDefault = rangeIsDefault;
235
240
  /** @property {number} - At what percentage of range should it start tapering */
236
241
  this.taper = taper;
237
242
  /** @property {number} - How much to randomize frequency each time sound plays
@@ -286,6 +291,15 @@ class Sound
286
291
  }
287
292
  }
288
293
 
294
+ /** World space max range of sound, 0 for no limit; a range that is set is the sound's own, in 3D too
295
+ * @type {number} */
296
+ get range() { return this.rangeValue; }
297
+ set range(range)
298
+ {
299
+ this.rangeValue = range;
300
+ this.rangeIsDefault = false;
301
+ }
302
+
289
303
  /** Sample data for each channel
290
304
  * Sounds keep their samples in an audio buffer, so reading this rebuilds
291
305
  * the arrays from it and caches them. The copies are safe to hold onto,
@@ -867,7 +881,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
867
881
  }
868
882
 
869
883
  ///////////////////////////////////////////////////////////////////////////////
870
- // ZzFXMicro - Zuper Zmall Zound Zynth - v1.3.2 by Frank Force
884
+ // ZzFXMicro - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
871
885
 
872
886
  /** Generate and play a ZzFX sound
873
887
  *
@@ -899,7 +913,7 @@ function zzfx(...zzfxSound) { return playSamples([zzfxG(...zzfxSound)]); }
899
913
  * @param {number} [decay] - Decay time, how long to reach sustain after attack (seconds)
900
914
  * @param {number} [tremolo] - Trembling effect, rate controlled by repeat time (percent)
901
915
  * @param {number} [filter] - Filter cutoff frequency, positive for HPF, negative for LPF (Hz)
902
- * @return {Array} - Array of audio samples
916
+ * @return {Float32Array} - The audio samples
903
917
  * @memberof Audio */
904
918
  function zzfxG
905
919
  (
@@ -937,7 +951,7 @@ function zzfxG
937
951
  crush = 0, // bit crush offset
938
952
  jump = 1, // pitch jump timer
939
953
  length, // sample length
940
- b = [], // sample buffer
954
+ b, // sample buffer
941
955
  t = 0, // sample time
942
956
  i = 0, // sample index
943
957
  s = 0, // sample value
@@ -964,9 +978,12 @@ function zzfxG
964
978
  pitchJumpTime *= sampleRate;
965
979
  repeatTime = repeatTime * sampleRate | 0;
966
980
 
981
+ // allocate the full sample buffer up front, much faster than growing an array
982
+ length = attack + decay + sustain + release + delay | 0;
983
+ b = new Float32Array(length > 0 ? length : 0);
984
+
967
985
  // generate waveform
968
- for (length = attack + decay + sustain + release + delay | 0;
969
- i < length; b[i++] = s * volume) // sample
986
+ for (; i < length; b[i++] = s * volume) // sample
970
987
  {
971
988
  if (!(++crush%(bitCrush*100|0))) // bit crush
972
989
  {
@@ -1002,7 +1019,7 @@ function zzfxG
1002
1019
 
1003
1020
  f = (frequency += slide += deltaSlide) *// frequency
1004
1021
  cos(modulation*modOffset++); // modulation
1005
- t += f + f*noise*sin(i**5); // noise
1022
+ t += f + f*noise*(i*i*PI2%2-1); // noise
1006
1023
 
1007
1024
  if (jump && ++jump > pitchJumpTime) // pitch jump
1008
1025
  {
@@ -50,12 +50,14 @@ const enginePluginFiles =
50
50
  `${PLUGIN_FOLDER}/textureSheet.js`,
51
51
  `${PLUGIN_FOLDER}/tweenSystem.js`,
52
52
  `${PLUGIN_FOLDER}/sceneSystem.js`,
53
+ `${PLUGIN_FOLDER}/parallax.js`,
53
54
  `${PLUGIN_FOLDER}/pathFinder.js`,
54
55
  `${PLUGIN_FOLDER}/math3d.js`,
55
56
  `${PLUGIN_FOLDER}/render3d.js`,
56
57
  `${PLUGIN_FOLDER}/render3dExtras.js`,
57
58
  `${PLUGIN_FOLDER}/render3dVoxels.js`,
58
59
  `${PLUGIN_FOLDER}/render3dLevel.js`,
60
+ `${PLUGIN_FOLDER}/particleEffects.js`,
59
61
  `${PLUGIN_FOLDER}/gltf.js`,
60
62
  `${PLUGIN_FOLDER}/threejs.js`,
61
63
  ];