littlejsengine 1.20.0 → 1.22.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/dist/littlejs.d.ts +1041 -81
- package/dist/littlejs.esm.js +6191 -379
- package/dist/littlejs.esm.min.js +22 -9
- package/dist/littlejs.js +6238 -476
- package/dist/littlejs.min.js +21 -8
- package/dist/littlejs.release.js +3500 -387
- package/package.json +3 -2
- package/plugins/gltf.js +61 -9
- package/plugins/lightSystem.js +44 -1
- package/plugins/math3d.js +132 -30
- package/plugins/particleEffects.js +745 -0
- package/plugins/pluginExport.js +42 -0
- package/plugins/postProcess.js +246 -39
- package/plugins/render3d.js +850 -182
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +2576 -0
- package/plugins/render3dExtras.js +421 -15
- package/plugins/render3dLevel.js +352 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +59 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +18 -9
- package/src/engineBuild.mjs +4 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +141 -7
- package/src/engineEditor.js +52 -22
- package/src/engineExport.js +8 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineParticles.js +80 -11
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
- package/src/engineWebGL.js +27 -10
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS 3D Level Plugin
|
|
3
|
+
* - A 3D level is a list of objects, each a type name, a position, and a rotation, scale and properties where
|
|
4
|
+
* they are not the default, kept as plain JSON that can be written by hand
|
|
5
|
+
* - level3DAddType names the types a game makes them from, as objectLayersAddType does in 2D
|
|
6
|
+
* - level3DLoad makes every object of a level
|
|
7
|
+
* - Box, Sphere, Cylinder and Light are built in, to block out and light a level with no code
|
|
8
|
+
* - The 3D level editor, in debug builds, edits the level a game loaded
|
|
9
|
+
* @namespace Level3D
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
15
|
+
|
|
16
|
+
const LEVEL3D_VERSION = 1; // the format a level's littlejs3D names
|
|
17
|
+
|
|
18
|
+
// the types a level's objects are made from, by name
|
|
19
|
+
const level3DTypes = new Map;
|
|
20
|
+
|
|
21
|
+
// the scale each object had when it was made, the level's scale multiplies it
|
|
22
|
+
const level3DBaseScale = new WeakMap;
|
|
23
|
+
|
|
24
|
+
/** Add a type of object, so level3DLoad makes one wherever a level has an object of that type
|
|
25
|
+
* - The name is a string because minified builds rename classes
|
|
26
|
+
* - A class, or any function with a prototype, is made with new make(pos3D, properties); an arrow function is
|
|
27
|
+
* called as make(pos3D, properties), for what is not an object, like a player start
|
|
28
|
+
* - properties is the defaults with the object's own values over them, and each of the type's own is also set on
|
|
29
|
+
* what was made; a property the type has no default for is in properties and is not set
|
|
30
|
+
* - Give a class a constructor of its own that takes the position: one that hands every argument on to
|
|
31
|
+
* EngineObject3D would hand it the properties as its mesh
|
|
32
|
+
* - An EngineObject3D then gets the object's rotation, and its scale times the scale it was made with
|
|
33
|
+
* - Adding a name again replaces it, Box, Sphere, Cylinder and Light too
|
|
34
|
+
* @param {string} name - The type the objects have in the level
|
|
35
|
+
* @param {Function} make - A class made at each object's position, or a function called with it
|
|
36
|
+
* @param {Object} [defaults] - Properties of each one made, the level editor shows inputs for them
|
|
37
|
+
* @param {TileInfo} [tileInfo] - An icon for the level editor
|
|
38
|
+
* @memberof Level3D
|
|
39
|
+
* @example
|
|
40
|
+
* level3DAddType('Crate', Crate, {health: 3});
|
|
41
|
+
* level3DAddType('PlayerStart', (pos)=> playerStart = pos); */
|
|
42
|
+
function level3DAddType(name, make, defaults={}, tileInfo)
|
|
43
|
+
{
|
|
44
|
+
ASSERT(isStringLike(name), 'object type name must be a string');
|
|
45
|
+
ASSERT(typeof make === 'function', 'make must be a class or function');
|
|
46
|
+
ASSERT(!!defaults && typeof defaults === 'object', 'defaults must be an object');
|
|
47
|
+
level3DTypes.set(String(name), {make, defaults, tileInfo});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Add a type that is a mesh and nothing more, a static prop with no class to write, for a built mesh or a model
|
|
51
|
+
* - Each object has a color and a solid property, solid collides as the box around the mesh
|
|
52
|
+
* @param {string} name - The type the objects have in the level
|
|
53
|
+
* @param {Mesh} mesh - Shared by every object of the type
|
|
54
|
+
* @param {TileInfo} [tileInfo] - Its texture
|
|
55
|
+
* @param {Color} [color] - Its color, an object's own color property goes over it
|
|
56
|
+
* @memberof Level3D
|
|
57
|
+
* @example
|
|
58
|
+
* level3DAddMesh('Tree', treeMesh, tile(4)); */
|
|
59
|
+
function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
60
|
+
{
|
|
61
|
+
ASSERT(mesh instanceof Mesh, 'mesh must be a Mesh');
|
|
62
|
+
ASSERT(isColor(color), 'color must be a color');
|
|
63
|
+
level3DAddType(name, function(pos, properties) { return level3DMakeShape(pos, properties, mesh, tileInfo); },
|
|
64
|
+
{color, solid: false}, tileInfo);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Make the objects of a level, each from the type added for its name
|
|
68
|
+
* - The level is an object: {littlejs3D: 1, objects: [{id, type, pos: [x, y, z]}, ...]}, and {} is a new one
|
|
69
|
+
* - rotation is pitch, yaw and roll in degrees, scale is a number for each axis, both left out when they are
|
|
70
|
+
* the default, and properties holds what differs from the type's defaults
|
|
71
|
+
* - A Color property is a #rrggbb or #rrggbbaa string, a Vector2 or Vector3 an array, as the default says
|
|
72
|
+
* - An object whose type was not added is skipped, with a warning in debug builds
|
|
73
|
+
* - What a file written by hand gets wrong uses the default: a value that is not of its default's type
|
|
74
|
+
* - An object its type can not make is skipped with an error in debug builds, where asserts throw, and the rest
|
|
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
|
|
82
|
+
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
83
|
+
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
84
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
|
|
85
|
+
* leaves out stays as the game set it, and a level with no block changes nothing
|
|
86
|
+
* @param {Object} level - The level, the level editor edits this same object
|
|
87
|
+
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
88
|
+
* @memberof Level3D */
|
|
89
|
+
function level3DLoad(level)
|
|
90
|
+
{
|
|
91
|
+
ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
92
|
+
ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
93
|
+
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
94
|
+
level3DSceneApply(level.scene);
|
|
95
|
+
const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
|
|
96
|
+
map && made.push(map);
|
|
97
|
+
const terrain = level3DTerrainMap = level3DTerrainMake(level.terrain);
|
|
98
|
+
terrain && made.push(terrain);
|
|
99
|
+
for (const object of isArray(level.objects) ? level.objects : [])
|
|
100
|
+
{
|
|
101
|
+
if (!object || typeof object !== 'object') continue;
|
|
102
|
+
const result = level3DMake(object);
|
|
103
|
+
editor3DObjectMade(object, result); // debug builds link it for the 3D editor
|
|
104
|
+
result && made.push(result);
|
|
105
|
+
}
|
|
106
|
+
return made;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// the sheet a level's block map is made with, and what sets each one up, see level3DVoxelSetup; and the map the
|
|
110
|
+
// level loaded last made, the one the 3D editor paints
|
|
111
|
+
let level3DVoxelTiles, level3DVoxelSetupMap, level3DVoxelMap;
|
|
112
|
+
|
|
113
|
+
/** How a level's block map is made: the sheet its blocks show tiles of, and a function to set it up
|
|
114
|
+
* - A level's voxels block makes a VoxelMap when the level loads, with texture 0 and the default tile size unless
|
|
115
|
+
* a sheet is given here; a block's type shows that tile of the sheet on every face
|
|
116
|
+
* - setup is called with each map a level makes, to give block types their own faces or make them see-through
|
|
117
|
+
* - Call it before level3DLoad; with no arguments the defaults are back
|
|
118
|
+
* @param {TileInfo} [tileInfo] - The sheet's first tile, as for a VoxelMap
|
|
119
|
+
* @param {function(VoxelMap): void} [setup]
|
|
120
|
+
* @example
|
|
121
|
+
* level3DVoxelSetup(tile(0, 16, 1), (map)=> map.setBlockType(1, {top: 0, side: 1, bottom: 2})); // grass
|
|
122
|
+
* @memberof Level3D */
|
|
123
|
+
function level3DVoxelSetup(tileInfo, setup)
|
|
124
|
+
{
|
|
125
|
+
level3DVoxelTiles = tileInfo;
|
|
126
|
+
level3DVoxelSetupMap = setup;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// the most cells a level's map may have, 256 each way
|
|
130
|
+
const LEVEL3D_VOXEL_CELLS = 2 ** 24;
|
|
131
|
+
|
|
132
|
+
// 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
|
|
133
|
+
function level3DVoxelsEncode(data)
|
|
134
|
+
{
|
|
135
|
+
const blocks = [];
|
|
136
|
+
for (let i = 0; i < data.length;)
|
|
137
|
+
{
|
|
138
|
+
let end = i + 1;
|
|
139
|
+
while (end < data.length && data[end] === data[i]) ++end;
|
|
140
|
+
blocks.push(end - i, data[i]);
|
|
141
|
+
i = end;
|
|
142
|
+
}
|
|
143
|
+
return blocks;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 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
|
|
147
|
+
// is not two numbers ends them
|
|
148
|
+
function level3DVoxelsDecode(blocks, data)
|
|
149
|
+
{
|
|
150
|
+
data.fill(0);
|
|
151
|
+
if (!isArray(blocks)) return;
|
|
152
|
+
for (let i = 0, cell = 0; i + 1 < blocks.length && cell < data.length; i += 2)
|
|
153
|
+
{
|
|
154
|
+
const count = blocks[i], type = blocks[i + 1];
|
|
155
|
+
if (!isNumber(count) || !isNumber(type) || count < 0) return;
|
|
156
|
+
const end = min(cell + floor(count), data.length);
|
|
157
|
+
data.fill(type >= 0 && type <= 255 && type % 1 === 0 ? type : 0, cell, end);
|
|
158
|
+
cell = end;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 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
|
|
163
|
+
function level3DVoxelsShape(voxels)
|
|
164
|
+
{
|
|
165
|
+
if (!voxels || typeof voxels !== 'object') return;
|
|
166
|
+
const size = level3DVector(voxels.size);
|
|
167
|
+
if (!size || [size.x, size.y, size.z].some((n)=> n < 1 || n % 1) ||
|
|
168
|
+
size.x * size.y * size.z > LEVEL3D_VOXEL_CELLS) return;
|
|
169
|
+
return {pos: level3DVector(voxels.pos, vec3()), size};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// the VoxelMap of a level's voxels block, undefined when it has none or it is wrong
|
|
173
|
+
function level3DVoxelsMake(voxels)
|
|
174
|
+
{
|
|
175
|
+
const shape = level3DVoxelsShape(voxels);
|
|
176
|
+
if (!shape || typeof VoxelMap == 'undefined') return;
|
|
177
|
+
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles || tile());
|
|
178
|
+
level3DVoxelsDecode(voxels.blocks, map.data);
|
|
179
|
+
level3DVoxelSetupMap?.(map);
|
|
180
|
+
map.rebuild();
|
|
181
|
+
return map;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// the terrain the level loaded last made, the one the 3D editor sculpts, and the most samples it may have a side
|
|
185
|
+
let level3DTerrainMap;
|
|
186
|
+
const LEVEL3D_TERRAIN_SAMPLES = 129;
|
|
187
|
+
|
|
188
|
+
// what a level's terrain block is made with: its center, its size in the world along x and z, how tall a full
|
|
189
|
+
// 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
|
|
190
|
+
// one a HeightMap can be made of
|
|
191
|
+
function level3DTerrainShape(terrain)
|
|
192
|
+
{
|
|
193
|
+
if (!terrain || typeof terrain !== 'object') return;
|
|
194
|
+
const size = terrain.size, rows = terrain.heights, n = LEVEL3D_TERRAIN_SAMPLES;
|
|
195
|
+
if (!isArray(size) || size.length !== 2 || !size.every((v)=> isNumber(v) && v > 0)) return;
|
|
196
|
+
if (!isArray(rows) || rows.length < 2 || rows.length > n || !isArray(rows[0])) return;
|
|
197
|
+
const columns = rows[0].length;
|
|
198
|
+
if (columns < 2 || columns > n || !rows.every((row)=> isArray(row) && row.length === columns)) return;
|
|
199
|
+
const heights = rows.map((row)=> row.map((v)=> isNumber(v) ? clamp(v) : 0));
|
|
200
|
+
return {pos: level3DVector(terrain.pos, vec3()), size: vec2(size[0], size[1]),
|
|
201
|
+
height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
205
|
+
function level3DTerrainMake(terrain)
|
|
206
|
+
{
|
|
207
|
+
const shape = level3DTerrainShape(terrain);
|
|
208
|
+
if (!shape || typeof HeightMap == 'undefined') return;
|
|
209
|
+
const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
|
|
210
|
+
if (/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(terrain.color))
|
|
211
|
+
map.color = new Color().setHex(terrain.color);
|
|
212
|
+
return map;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// the sky domes levels made, a level's sky takes the place of the one before and disposes it; a dome the game made
|
|
216
|
+
// itself is left whole, for the game to put back
|
|
217
|
+
const level3DSkies = new WeakSet;
|
|
218
|
+
|
|
219
|
+
// set the scene a level's scene block gives: sky as its top, horizon and bottom colors, with ambient how much of
|
|
220
|
+
// them lights the scene, sunDirection and sunColor, fog as its start and end with fogColor, the horizon without one,
|
|
221
|
+
// and shadows; a setting the block does not have, or has wrong, stays as it is
|
|
222
|
+
function level3DSceneApply(scene)
|
|
223
|
+
{
|
|
224
|
+
const r = render3D;
|
|
225
|
+
if (!r || !scene || typeof scene !== 'object') return;
|
|
226
|
+
const color = (value)=> /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) ? new Color().setHex(value) : undefined;
|
|
227
|
+
const sky = isArray(scene.sky) && scene.sky.length === 3 ? scene.sky.map(color) : [];
|
|
228
|
+
if (sky.length && sky.every((c)=> c))
|
|
229
|
+
{
|
|
230
|
+
// as setSky does, without disposing a dome that is the game's own
|
|
231
|
+
const [top, horizon, bottom] = sky, ambient = isNumber(scene.ambient) ? scene.ambient : .5;
|
|
232
|
+
level3DSkies.has(r.sky) && r.sky.dispose();
|
|
233
|
+
level3DSkies.add(r.sky = buildSky(top, horizon, bottom));
|
|
234
|
+
r.fogColor = horizon.copy();
|
|
235
|
+
r.ambientColor = top.scale(ambient, 1);
|
|
236
|
+
r.ambientGroundColor = bottom.scale(ambient, 1);
|
|
237
|
+
}
|
|
238
|
+
const sun = level3DVector(scene.sunDirection);
|
|
239
|
+
if (sun && sun.lengthSquared())
|
|
240
|
+
r.sunDirection = sun;
|
|
241
|
+
r.sunColor = color(scene.sunColor) || r.sunColor;
|
|
242
|
+
const fog = scene.fog;
|
|
243
|
+
if (isArray(fog) && fog.length === 2 && fog.every((v)=> isNumber(v)))
|
|
244
|
+
r.fogStart = fog[0], r.fogEnd = fog[1];
|
|
245
|
+
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
246
|
+
if (typeof scene.shadows === 'boolean')
|
|
247
|
+
r.shadows = scene.shadows;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
251
|
+
function level3DVector(value, fallback)
|
|
252
|
+
{
|
|
253
|
+
return isArray(value) && value.length === 3 && value.every((v)=> isNumber(v)) ?
|
|
254
|
+
vec3(value[0], value[1], value[2]) : fallback;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// the defaults of an object's type, a Color or vector copied for each object, then its own properties over them,
|
|
258
|
+
// each read as its default is; one the file got wrong, a value not of its default's type, keeps the default, and
|
|
259
|
+
// one the type has no default for is kept as it is
|
|
260
|
+
function level3DProperties(type, object)
|
|
261
|
+
{
|
|
262
|
+
const properties = {}, own = object.properties;
|
|
263
|
+
for (const [key, value] of Object.entries(type.defaults))
|
|
264
|
+
properties[key] = value?.copy ? value.copy() : value;
|
|
265
|
+
for (const [key, value] of Object.entries(own && typeof own === 'object' ? own : {}))
|
|
266
|
+
{
|
|
267
|
+
const d = type.defaults[key];
|
|
268
|
+
if (isColor(d))
|
|
269
|
+
/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) && (properties[key] = new Color().setHex(value));
|
|
270
|
+
else if (isVector3(d))
|
|
271
|
+
properties[key] = level3DVector(value, properties[key]);
|
|
272
|
+
else if (isVector2(d))
|
|
273
|
+
isArray(value) && value.length === 2 && value.every((v)=> isNumber(v)) &&
|
|
274
|
+
(properties[key] = vec2(value[0], value[1]));
|
|
275
|
+
else if (d === undefined || typeof value === typeof d)
|
|
276
|
+
properties[key] = value;
|
|
277
|
+
}
|
|
278
|
+
return properties;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// make one object of a level from the type added for its name, undefined when there is no such type or it made
|
|
282
|
+
// nothing
|
|
283
|
+
function level3DMake(object)
|
|
284
|
+
{
|
|
285
|
+
const type = level3DTypes.get(object.type);
|
|
286
|
+
if (!type)
|
|
287
|
+
{
|
|
288
|
+
debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const pos = level3DVector(object.pos, vec3()), properties = level3DProperties(type, object);
|
|
292
|
+
const {make} = type, count = engineObjects.length;
|
|
293
|
+
let result;
|
|
294
|
+
try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
|
|
295
|
+
catch (error)
|
|
296
|
+
{
|
|
297
|
+
// a value the type can not be made with: in debug builds, where asserts throw, the object is skipped with
|
|
298
|
+
// what it made so far, so the rest of the level loads and the level editor can put the value right
|
|
299
|
+
if (!debug) throw error;
|
|
300
|
+
console.error(`level3DLoad: ${object.type} ${object.id} could not be made, skipped:`, error);
|
|
301
|
+
for (const o of engineObjects.slice(count))
|
|
302
|
+
o.destroy();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!result || typeof result !== 'object') return;
|
|
306
|
+
if (result instanceof EngineObject3D)
|
|
307
|
+
{
|
|
308
|
+
level3DBaseScale.set(result, result.scale3D.copy());
|
|
309
|
+
result.rotation3D = level3DVector(object.rotation, vec3()).scale(PI / 180);
|
|
310
|
+
result.scale3D = result.scale3D.multiply(level3DVector(object.scale, vec3(1)));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// the type's own properties are set on it, one the type has no default for could be a field of the engine's
|
|
314
|
+
for (const key in type.defaults)
|
|
315
|
+
result[key] = properties[key];
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
320
|
+
// the built-in types
|
|
321
|
+
|
|
322
|
+
// a static object of a mesh, with the color, tile and solid properties the built-in types have
|
|
323
|
+
function level3DMakeShape(pos, properties, mesh, tileInfo, asSphere=false)
|
|
324
|
+
{
|
|
325
|
+
const o = new EngineObject3D(pos, mesh, properties.tile >= 0 ? tile(properties.tile) : tileInfo, properties.color);
|
|
326
|
+
o.collideAsSphere3D = asSphere;
|
|
327
|
+
properties.solid && o.setCollision();
|
|
328
|
+
return o;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// the cylinder all Cylinder objects share, built the first time one is made
|
|
332
|
+
let level3DCylinderMesh;
|
|
333
|
+
|
|
334
|
+
level3DAddType('Box', function(pos, properties) { return level3DMakeShape(pos, properties, render3D.boxMesh); },
|
|
335
|
+
{color: WHITE, tile: -1, solid: true});
|
|
336
|
+
level3DAddType('Sphere', function(pos, properties)
|
|
337
|
+
{ return level3DMakeShape(pos, properties, render3D.sphereMesh, undefined, true); },
|
|
338
|
+
{color: WHITE, tile: -1, solid: true});
|
|
339
|
+
level3DAddType('Cylinder', function(pos, properties)
|
|
340
|
+
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
341
|
+
{color: WHITE, tile: -1, solid: true});
|
|
342
|
+
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
343
|
+
// object's rotation aims it, softness is how much of the cone fades, and shadows makes it the one that casts them
|
|
344
|
+
level3DAddType('Light', function(pos, properties)
|
|
345
|
+
{
|
|
346
|
+
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
347
|
+
light.coneAngle = max(properties.cone, 0) * PI / 180;
|
|
348
|
+
light.coneSoftness = properties.softness;
|
|
349
|
+
if (properties.shadows && render3D)
|
|
350
|
+
render3D.shadowLight = light;
|
|
351
|
+
return light;
|
|
352
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
@@ -465,6 +465,18 @@ class VoxelMap extends EngineObject3D
|
|
|
465
465
|
* @ignore */
|
|
466
466
|
levelRaycast3D(ray) { return this.raycast(ray)?.distance; }
|
|
467
467
|
|
|
468
|
+
/** Where a short move goes into a block, for a particle's move in one frame: the part of the move made before it,
|
|
469
|
+
* 0 to 1, and the normal of the face it comes in through; undefined when it hits none, or starts inside one
|
|
470
|
+
* @param {Vector3} from
|
|
471
|
+
* @param {Vector3} to
|
|
472
|
+
* @return {{distance: number, normal: Vector3}|undefined}
|
|
473
|
+
* @ignore */
|
|
474
|
+
levelSegment3D(from, to)
|
|
475
|
+
{
|
|
476
|
+
const hit = this.raycast(new Ray3D(from, to.subtract(from)), 1);
|
|
477
|
+
return hit && hit.distance > 0 ? {distance: hit.distance, normal: hit.normal} : undefined;
|
|
478
|
+
}
|
|
479
|
+
|
|
468
480
|
/** Keeps an eye on its placement, called automatically each frame */
|
|
469
481
|
update()
|
|
470
482
|
{
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Scene System Plugin
|
|
3
|
+
* - A game's states, like a title, the game and game over, as scene objects
|
|
4
|
+
* - setScene leaves the current scene, destroys the objects and enters the next
|
|
5
|
+
* - Objects with the persistent flag set outlive every switch
|
|
6
|
+
* - Pause is the engine's paused flag, a scene's update stops with gameUpdate
|
|
7
|
+
* @namespace SceneSystem
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
13
|
+
|
|
14
|
+
/** A scene, any object with the hooks it needs, each optional and called as a method so this is the scene
|
|
15
|
+
* @typedef {Object} Scene
|
|
16
|
+
* @property {function():void} [enter] - Called when the scene starts, after the old one left and the objects were destroyed
|
|
17
|
+
* @property {function():void} [leave] - Called before the next scene starts, while the scene is current and its objects are still there
|
|
18
|
+
* @property {function():void} [update] - Called each update after gameUpdate, not while paused or at time scale 0
|
|
19
|
+
* @property {function():void} [render] - Called before gameRender, to draw under the objects
|
|
20
|
+
* @property {function():void} [renderPost] - Called after gameRenderPost, to draw over the game, while paused too; plugins made after the first setScene draw over it
|
|
21
|
+
* @memberof SceneSystem */
|
|
22
|
+
|
|
23
|
+
let sceneCurrent, sceneLeaving = false, scenePluginAdded = false;
|
|
24
|
+
|
|
25
|
+
/** Leave the current scene, destroy every object that is not persistent, and enter the next scene
|
|
26
|
+
* - Setting the current scene again restarts it, and no scene leaves the game with none
|
|
27
|
+
* - The switch happens at once, it can be called from anywhere but a scene's leave
|
|
28
|
+
* @param {Scene} [scene] - The scene to enter
|
|
29
|
+
* @memberof SceneSystem */
|
|
30
|
+
function setScene(scene)
|
|
31
|
+
{
|
|
32
|
+
ASSERT(!sceneLeaving, 'setScene can not be called from a scene leave');
|
|
33
|
+
if (!scenePluginAdded)
|
|
34
|
+
{
|
|
35
|
+
scenePluginAdded = true;
|
|
36
|
+
engineAddPlugin(sceneUpdate, sceneRenderPost, undefined, undefined, sceneRender);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (sceneCurrent)
|
|
40
|
+
{
|
|
41
|
+
// a leave that throws still lets the next switch through
|
|
42
|
+
sceneLeaving = true;
|
|
43
|
+
try { sceneCurrent.leave?.(); }
|
|
44
|
+
finally { sceneLeaving = false; }
|
|
45
|
+
}
|
|
46
|
+
engineObjectsDestroy();
|
|
47
|
+
sceneCurrent = scene;
|
|
48
|
+
scene?.enter?.();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Get the current scene
|
|
52
|
+
* @return {Scene|undefined}
|
|
53
|
+
* @memberof SceneSystem */
|
|
54
|
+
function getScene() { return sceneCurrent; }
|
|
55
|
+
|
|
56
|
+
// the plugin hooks, update skips the frozen ticks that skip gameUpdate
|
|
57
|
+
function sceneUpdate() { paused || !timeScale || sceneCurrent?.update?.(); }
|
|
58
|
+
function sceneRender() { sceneCurrent?.render?.(); }
|
|
59
|
+
function sceneRenderPost() { sceneCurrent?.renderPost?.(); }
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -596,7 +596,9 @@ function tweenUpdate(gameDelta, realDelta)
|
|
|
596
596
|
for (let i = list.length; i--;)
|
|
597
597
|
{
|
|
598
598
|
const t = list[i];
|
|
599
|
-
|
|
599
|
+
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
600
|
+
// counts on from this one
|
|
601
|
+
if (!t.active || t.activePass >= pass) continue;
|
|
600
602
|
let dt;
|
|
601
603
|
if (enginePath)
|
|
602
604
|
{
|
|
@@ -620,7 +622,7 @@ function tweenUpdate(gameDelta, realDelta)
|
|
|
620
622
|
// Completion: fire end value, remove from active, start the next iteration
|
|
621
623
|
// of a loop or pingPong, or when there is none it has completed, fire onComplete
|
|
622
624
|
t.callback(t.interp(0));
|
|
623
|
-
if (!t.active || t.activePass
|
|
625
|
+
if (!t.active || t.activePass >= pass)
|
|
624
626
|
continue; // stopped or restarted by its own callback, the run it was on ends without completing
|
|
625
627
|
tweenDeactivate(t);
|
|
626
628
|
const next = t.thenCallback;
|