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.
- package/README.md +1 -1
- package/dist/littlejs.d.ts +1258 -141
- package/dist/littlejs.esm.js +5023 -429
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +4977 -430
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +2759 -313
- package/package.json +3 -2
- package/plugins/gltf.js +7 -1
- package/plugins/lightSystem.js +50 -5
- package/plugins/math3d.js +42 -20
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +760 -0
- package/plugins/pluginExport.js +40 -0
- package/plugins/postProcess.js +206 -28
- package/plugins/render3d.js +495 -188
- package/plugins/render3dEditor.js +1754 -80
- package/plugins/render3dExtras.js +294 -21
- package/plugins/render3dLevel.js +463 -6
- package/plugins/sceneSystem.js +3 -2
- package/src/engine.js +1 -1
- package/src/engineAudio.js +25 -8
- package/src/engineBuild.mjs +2 -0
- package/src/engineDraw.js +59 -4
- package/src/engineEditor.js +475 -37
- package/src/engineExport.js +7 -0
- package/src/engineInput.js +48 -6
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
- package/src/engineWebGL.js +27 -10
package/plugins/render3dLevel.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
155
|
-
result.scale3D = result.scale3D.multiply(
|
|
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
|
-
|
|
189
|
-
|
|
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});
|
package/plugins/sceneSystem.js
CHANGED
|
@@ -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
package/src/engineAudio.js
CHANGED
|
@@ -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
|
|
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.
|
|
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 {
|
|
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
|
|
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 (
|
|
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*
|
|
1022
|
+
t += f + f*noise*(i*i*PI2%2-1); // noise
|
|
1006
1023
|
|
|
1007
1024
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
1008
1025
|
{
|
package/src/engineBuild.mjs
CHANGED
|
@@ -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
|
];
|