littlejsengine 1.19.3 → 1.21.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.
Files changed (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. package/plugins/box2d.wasm.wasm +0 -0
@@ -0,0 +1,514 @@
1
+ /*
2
+ * LittleJS 3D Voxels Plugin
3
+ * - VoxelMap: a grid of blocks drawn with the faces between them left out, that objects with collideLevel collide
4
+ * with, the 3D twin of the 2D tile layers
5
+ * - Requires the Render3D plugin and goes after it, everything here is part of its Render3D namespace
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ ///////////////////////////////////////////////////////////////////////////////
11
+
12
+ // cells along each side of a chunk, each chunk its own mesh, built again only when something in or beside it changes
13
+ const RENDER3D_VOXEL_CHUNK = 16;
14
+
15
+ // each face of a block: its normal, then the right and up axes across it, right cross up is the normal, as buildBox
16
+ // has them, in the order setBlockType takes six faces: +x, -x, +y, -y, +z, -z
17
+ const RENDER3D_VOXEL_FACES = [
18
+ [vec3( 1, 0, 0), vec3(0, 0, -1), vec3(0, 1, 0)],
19
+ [vec3(-1, 0, 0), vec3(0, 0, 1), vec3(0, 1, 0)],
20
+ [vec3(0, 1, 0), vec3(1, 0, 0), vec3(0, 0, -1)],
21
+ [vec3(0, -1, 0), vec3(1, 0, 0), vec3(0, 0, 1)],
22
+ [vec3(0, 0, 1), vec3(1, 0, 0), vec3(0, 1, 0)],
23
+ [vec3(0, 0, -1), vec3(-1, 0, 0), vec3(0, 1, 0)],
24
+ ];
25
+
26
+ // the shade of a corner by how many solid blocks crowd it, from 0, closed in, to 3, open
27
+ const RENDER3D_VOXEL_SHADES = [.55, .7, .85, 1].map(v=> Object.freeze(rgb(v, v, v)));
28
+
29
+ // each face's corners about a cell's center, as render3DQuadAxes gives them: top left, bottom left, top right, bottom
30
+ // right; then the sides of the face each corner is toward, for its shading, and the corners in strip order, or split
31
+ // along the other diagonal
32
+ const RENDER3D_VOXEL_CORNERS = RENDER3D_VOXEL_FACES.map(([normal, right, up])=>
33
+ render3DQuadAxes(normal.scale(.5), right.scale(.5), up.scale(.5)));
34
+ const RENDER3D_VOXEL_CORNER_SIDES = [[-1, 1], [-1, -1], [1, 1], [1, -1]];
35
+ const RENDER3D_VOXEL_ORDER = [0, 1, 2, 3], RENDER3D_VOXEL_FLIPPED = [1, 3, 0, 2];
36
+
37
+ /** What VoxelMap.raycast finds: how far along the ray, the block's cell and type, and the normal of the face it comes in
38
+ * through
39
+ * @typedef {{distance: number, cell: Vector3, normal: Vector3, type: number}} VoxelHit
40
+ * @memberof Render3D */
41
+
42
+ /**
43
+ * VoxelMap - A grid of blocks, a 3D tile map: it draws itself, and objects with collideLevel collide with it
44
+ * - pos3D is its corner, as a 2D tile layer's is, and each cell is one world unit, so cell (x, y, z) fills
45
+ * pos3D + (x..x+1, y..y+1, z..z+1); it stays upright and unscaled at the root
46
+ * - A block's type is a number from 1 to 255, 0 is empty; a type shows that tile of the sheet on every face unless
47
+ * setBlockType gives it its own faces, or makes it see-through or transparent
48
+ * - Faces between blocks are left out, and the map is drawn in chunks of 16 cells a side, a chunk built again only
49
+ * when a block in or beside it changes
50
+ * - Objects with collideLevel collide with it, see EngineObject3D.collideWithVoxel, and raycast finds the block a ray
51
+ * hits and the face it comes in through
52
+ * @extends EngineObject3D
53
+ * @memberof Render3D
54
+ * @example
55
+ * const map = new VoxelMap(vec3(), vec3(32, 16, 32), tile(0, 16));
56
+ * map.setBlockType(1, {top: 0, side: 1, bottom: 2}); // grass
57
+ * map.setVoxel(vec3(3, 0, 5), 1);
58
+ */
59
+ class VoxelMap extends EngineObject3D
60
+ {
61
+ /** Create a voxel map, it draws itself and joins the level's collision
62
+ * @param {Vector3} [pos3D] - Its corner
63
+ * @param {Vector3} [mapSize] - Cells along X, Y and Z
64
+ * @param {TileInfo} [tileInfo] - The sheet's first tile, as for a TileLayer, a block's type counts tiles from it;
65
+ * give the tiles a border of their own edge pixels, 2 or more, and that padding, or the blurred mipmaps of the
66
+ * distance blend each tile with the ones beside it and show seams along the block edges */
67
+ constructor(pos3D=vec3(), mapSize=vec3(16), tileInfo=tile())
68
+ {
69
+ super(pos3D);
70
+ ASSERT(isVector3(mapSize) && mapSize.x >= 1 && mapSize.y >= 1 && mapSize.z >= 1, 'mapSize must be at least 1 cell each way');
71
+ ASSERT(tileInfo instanceof TileInfo, 'tileInfo must be a TileInfo, the first tile of the sheet');
72
+ /** @property {Vector3} - Cells along X, Y and Z */
73
+ this.mapSize = mapSize.floor();
74
+ /** @property {TileInfo} - The sheet's first tile, rebuild() after changing it or resizing its texture */
75
+ this.tileInfo = tileInfo;
76
+ /** @property {Uint8Array} - The block type of each cell, x + mapSize.x * (y + mapSize.y * z), 0 empty; call
77
+ * rebuild() after changing it directly */
78
+ this.data = new Uint8Array(this.mapSize.x * this.mapSize.y * this.mapSize.z);
79
+ /** @property {boolean} - Darken the corners where blocks meet, rebuild() after changing it */
80
+ this.ambientOcclusion = true;
81
+ this.size3D = vec3(); // not a thing to collect or push, its cells are the solid part
82
+
83
+ const n = RENDER3D_VOXEL_CHUNK, s = this.mapSize;
84
+ this.chunkCount = vec3(ceil(s.x / n), ceil(s.y / n), ceil(s.z / n));
85
+ /** @type {Array<Mesh|undefined>} */
86
+ this.chunkMeshes = []; // each chunk's solid and see-through blocks, drawn opaque
87
+ /** @type {Array<Mesh|undefined>} */
88
+ this.chunkTransparentMeshes = []; // each chunk's transparent blocks, drawn blended
89
+ /** @type {Array<Vector3>} */
90
+ this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
91
+ this.chunksChanged = new Set;
92
+ /** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
93
+ this.blockTypes = [];
94
+ this.tiles = new Map; // tile index to the uvs of its corners
95
+
96
+ // the transparent blocks draw in the transparent stage, blended and sorted, through a child that draws them
97
+ const glass = new EngineObject3D;
98
+ glass.size3D = vec3(); // not a thing to collect either
99
+ glass.transparent = true;
100
+ glass.render3D = ()=> this.renderChunks(true);
101
+ this.addChild(glass);
102
+ render3DLevel.push(this);
103
+ }
104
+
105
+ /** The block type at a cell, 0 for empty or outside the map
106
+ * @param {Vector3} cell
107
+ * @return {number} */
108
+ getVoxel(cell)
109
+ {
110
+ ASSERT(isVector3(cell), 'cell must be a vec3');
111
+ return this.voxelAt(floor(cell.x), floor(cell.y), floor(cell.z));
112
+ }
113
+
114
+ /** The block type at whole number cell coordinates, 0 outside
115
+ * @param {number} x
116
+ * @param {number} y
117
+ * @param {number} z
118
+ * @return {number}
119
+ * @ignore */
120
+ voxelAt(x, y, z)
121
+ {
122
+ const s = this.mapSize;
123
+ return x < 0 || y < 0 || z < 0 || x >= s.x || y >= s.y || z >= s.z ? 0 : this.data[x + s.x * (y + s.y * z)];
124
+ }
125
+
126
+ /** Set the block at a cell, 0 clears it; a cell outside the map is ignored
127
+ * @param {Vector3} cell
128
+ * @param {number} type - 0 to 255 */
129
+ setVoxel(cell, type)
130
+ {
131
+ ASSERT(isVector3(cell), 'cell must be a vec3');
132
+ ASSERT(type >= 0 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 0 to 255', type);
133
+ const x = floor(cell.x), y = floor(cell.y), z = floor(cell.z), s = this.mapSize;
134
+ if (x < 0 || y < 0 || z < 0 || x >= s.x || y >= s.y || z >= s.z) return;
135
+ const i = x + s.x * (y + s.y * z);
136
+ if (this.data[i] === type) return;
137
+ this.data[i] = type;
138
+ // its chunk, and the chunks of the blocks around it: face to face it can show or hide their faces, and with
139
+ // corner shading on, the blocks all around it take shade from it
140
+ this.markChunk(x, y, z);
141
+ for (let dz = -1; dz <= 1; ++dz)
142
+ for (let dy = -1; dy <= 1; ++dy)
143
+ for (let dx = -1; dx <= 1; ++dx)
144
+ if ((this.ambientOcclusion || abs(dx) + abs(dy) + abs(dz) === 1) && this.voxelAt(x + dx, y + dy, z + dz))
145
+ this.markChunk(x + dx, y + dy, z + dz);
146
+ }
147
+
148
+ /** Mark the chunk holding a cell as changed, a cell outside the map has none
149
+ * @param {number} x
150
+ * @param {number} y
151
+ * @param {number} z
152
+ * @ignore */
153
+ markChunk(x, y, z)
154
+ {
155
+ const s = this.mapSize, c = this.chunkCount, n = RENDER3D_VOXEL_CHUNK;
156
+ if (x < 0 || y < 0 || z < 0 || x >= s.x || y >= s.y || z >= s.z) return;
157
+ this.chunksChanged.add(floor(x / n) + c.x * (floor(y / n) + c.y * floor(z / n)));
158
+ }
159
+
160
+ /** Give a block type its own faces, or make it see-through or transparent
161
+ * @param {number} type - 1 to 255
162
+ * @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
163
+ * six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
164
+ * @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes in its texture, like
165
+ * leaves, so the blocks beside it keep their faces; transparent to blend, like glass or water, drawn in the
166
+ * transparent stage, and see-through too */
167
+ setBlockType(type, faces, {seeThrough=false, transparent=false}={})
168
+ {
169
+ ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
170
+ const f = /** @type {any} */ (faces);
171
+ const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
172
+ [f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
173
+ ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
174
+ this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
175
+ this.rebuild();
176
+ }
177
+
178
+ /** A block type's faces and how it is seen through
179
+ * @param {number} type
180
+ * @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
181
+ * @ignore */
182
+ blockType(type)
183
+ {
184
+ return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false};
185
+ }
186
+
187
+ /** Build every chunk again, after changing data directly or ambientOcclusion */
188
+ rebuild()
189
+ {
190
+ this.tiles.clear(); // the tile uvs too, the texture may have changed
191
+ const c = this.chunkCount;
192
+ for (let i = c.x * c.y * c.z; i--;)
193
+ this.chunksChanged.add(i);
194
+ }
195
+
196
+ /** Build the chunks that changed, the map calls it before it draws */
197
+ buildChunks()
198
+ {
199
+ for (const index of this.chunksChanged)
200
+ this.buildChunk(index);
201
+ this.chunksChanged.clear();
202
+ }
203
+
204
+ /** Build one chunk's meshes, a face for each block side that shows
205
+ * @param {number} index
206
+ * @ignore */
207
+ buildChunk(index)
208
+ {
209
+ const c = this.chunkCount, n = RENDER3D_VOXEL_CHUNK, s = this.mapSize, data = this.data;
210
+ const x0 = index % c.x * n, y0 = floor(index / c.x) % c.y * n, z0 = floor(index / (c.x * c.y)) * n;
211
+ const x1 = min(x0 + n, s.x), y1 = min(y0 + n, s.y), z1 = min(z0 + n, s.z);
212
+ const center = vec3((x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2);
213
+ const opaque = new Mesh, transparent = new Mesh, ao = this.ambientOcclusion;
214
+ // a block that shades a corner, the see-through ones let the light by
215
+ const solid = (x, y, z)=> { const t = this.voxelAt(x, y, z); return t && !this.blockType(t).seeThrough ? 1 : 0; };
216
+ const points = [], uvs = [], colors = [], shade = []; // one face at a time, addStrip copies them out
217
+ for (let z = z0; z < z1; ++z)
218
+ for (let y = y0; y < y1; ++y)
219
+ for (let x = x0; x < x1; ++x)
220
+ {
221
+ const type = data[x + s.x * (y + s.y * z)];
222
+ if (!type) continue;
223
+ const block = this.blockType(type), mesh = block.transparent ? transparent : opaque;
224
+ const cx = x + .5 - center.x, cy = y + .5 - center.y, cz = z + .5 - center.z;
225
+ for (let f = 0; f < 6; ++f)
226
+ {
227
+ // a face shows toward an empty cell, or a see-through block of another type
228
+ const [normal, right, up] = RENDER3D_VOXEL_FACES[f];
229
+ const bx = x + normal.x, by = y + normal.y, bz = z + normal.z, next = this.voxelAt(bx, by, bz);
230
+ if (next && (next === type || !this.blockType(next).seeThrough)) continue;
231
+
232
+ // each corner darkened by the solid blocks beside it on the face's side, the usual voxel shading
233
+ for (let k = 0; k < 4; ++k)
234
+ {
235
+ const [r, u] = RENDER3D_VOXEL_CORNER_SIDES[k];
236
+ const side1 = ao && solid(bx + right.x * r, by + right.y * r, bz + right.z * r);
237
+ const side2 = ao && solid(bx + up.x * u, by + up.y * u, bz + up.z * u);
238
+ const corner = ao && solid(bx + right.x * r + up.x * u, by + right.y * r + up.y * u, bz + right.z * r + up.z * u);
239
+ shade[k] = side1 && side2 ? 0 : 3 - +side1 - +side2 - +corner;
240
+ }
241
+
242
+ // its corners as a strip around the chunk's center, split along the diagonal that keeps the shading
243
+ // even, the other one shows a seam
244
+ const corners = RENDER3D_VOXEL_CORNERS[f], quad = this.tileUVs(block.faces[f]);
245
+ const order = shade[0] + shade[3] > shade[1] + shade[2] ? RENDER3D_VOXEL_FLIPPED : RENDER3D_VOXEL_ORDER;
246
+ for (let k = 0; k < 4; ++k)
247
+ {
248
+ const i = order[k], p = corners[i];
249
+ points[k] = vec3(cx + p.x, cy + p.y, cz + p.z);
250
+ uvs[k] = quad[i];
251
+ colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
252
+ }
253
+ mesh.addStrip(points, normal, uvs, colors);
254
+ }
255
+ }
256
+ this.chunkMeshes[index]?.dispose();
257
+ this.chunkTransparentMeshes[index]?.dispose();
258
+ this.chunkMeshes[index] = opaque.points.length ? opaque : undefined;
259
+ this.chunkTransparentMeshes[index] = transparent.points.length ? transparent : undefined;
260
+ this.chunkCenters[index] = center;
261
+ }
262
+
263
+ /** The uvs of a tile's corners, top left, bottom left, top right, bottom right, the tile counted from the map's
264
+ * first tile as a TileLayer counts them; kept, and shared by every face that shows it
265
+ * @param {number} index
266
+ * @return {Array<Vector2>}
267
+ * @ignore */
268
+ tileUVs(index)
269
+ {
270
+ let uvs = this.tiles.get(index);
271
+ if (!uvs)
272
+ {
273
+ const first = this.tileInfo;
274
+ const rect = render3DGetTileUVs(first.columns ? first.frame(index) : first.index(index)); // a shared rect
275
+ uvs = RENDER3D_QUAD_UVS.map(q=> Object.freeze(vec2(rect.x + q.x * rect.w, rect.y + q.y * rect.h)));
276
+ this.tiles.set(index, uvs);
277
+ }
278
+ return uvs;
279
+ }
280
+
281
+ /** Whether a box hits a block that stops the object, see EngineObject3D.collideWithVoxel
282
+ * @param {Vector3} pos - Center of the box in the world
283
+ * @param {Vector3} size
284
+ * @param {EngineObject3D} o
285
+ * @return {boolean}
286
+ * @ignore */
287
+ boxBlocked(pos, size, o)
288
+ {
289
+ // the cells the box is in along an axis, one only touching a cell's face is not in it, but a point, a box with
290
+ // no size, is in the cell it is at, even on a whole number where the two ends pass each other
291
+ const m = this.pos3D, s = this.mapSize, tiny = 1e-9;
292
+ const cells = (center, half, corner, count)=>
293
+ {
294
+ const first = floor(center - half - corner + tiny), last = max(ceil(center + half - corner - tiny) - 1, first);
295
+ return [max(first, 0), min(last, count - 1)];
296
+ };
297
+ const [x0, x1] = cells(pos.x, size.x / 2, m.x, s.x);
298
+ const [y0, y1] = cells(pos.y, size.y / 2, m.y, s.y);
299
+ const [z0, z1] = cells(pos.z, size.z / 2, m.z, s.z);
300
+ for (let z = z0; z <= z1; ++z)
301
+ for (let y = y0; y <= y1; ++y)
302
+ for (let x = x0; x <= x1; ++x)
303
+ {
304
+ const type = this.data[x + s.x * (y + s.y * z)];
305
+ if (type && o.collideWithVoxel(type, vec3(x, y, z)))
306
+ return true;
307
+ }
308
+ return false;
309
+ }
310
+
311
+ /** Keep an object out of the blocks, one axis at a time as 2D tiles do, called by the engine for each object with
312
+ * collideLevel; a sphere collides as its box, and one moving more than about a cell a frame can pass through
313
+ * - One already in blocks, as when a block is set on it, is pushed up to stand on those in its lower half when
314
+ * there is room, or else left free to move out of them, but not into any it is not in already
315
+ * @param {EngineObject3D} o
316
+ * @param {Vector3} oldPos - Where it was before it moved
317
+ * @ignore */
318
+ levelCollide3D(o, oldPos)
319
+ {
320
+ const k = o.scale3D, size = vec3(o.size3D.x * abs(k.x), o.size3D.y * abs(k.y), o.size3D.z * abs(k.z));
321
+ const p = o.pos3D, v = o.velocity3D, m = this.pos3D, epsilon = 1e-4;
322
+ if (!this.boxBlocked(p, size, o)) return;
323
+
324
+ if (this.boxBlocked(oldPos, size, o))
325
+ {
326
+ // the top of the highest blocked layer from its bottom up to its middle, each layer a flat box
327
+ const half = size.y / 2, flat = vec3(size.x, 0, size.z);
328
+ let top;
329
+ for (let y = floor(oldPos.y - half - m.y); y <= floor(oldPos.y - m.y); ++y)
330
+ if (this.boxBlocked(vec3(oldPos.x, m.y + y + .5, oldPos.z), flat, o))
331
+ top = y + 1;
332
+ const up = top === undefined ? undefined : vec3(oldPos.x, m.y + top + half + epsilon, oldPos.z);
333
+ if (up && !this.boxBlocked(up, size, o))
334
+ {
335
+ p.set(up.x, up.y, up.z);
336
+ v.y = max(v.y, 0);
337
+ o.groundObject = this;
338
+ return;
339
+ }
340
+
341
+ // or else free to move out, each axis alone, stopped only by a block it would enter that it is not in
342
+ // already: the cells its leading face passes into beyond those the box reached
343
+ const tiny = 1e-9, place = oldPos.copy();
344
+ for (const axis of ['y', 'x', 'z'])
345
+ {
346
+ const move = p[axis] - place[axis], halfSize = size[axis] / 2, corner = m[axis];
347
+ if (!move) continue;
348
+ const lead = (at)=> move > 0 ? ceil(at + halfSize - corner - tiny) - 1 : floor(at - halfSize - corner + tiny);
349
+ const from = lead(place[axis]), to = lead(p[axis]);
350
+ const first = min(from, to) + (move > 0 ? 1 : 0), last = max(from, to) - (move > 0 ? 0 : 1);
351
+ const slab = place.copy(), slabSize = size.copy();
352
+ slab[axis] = corner + (first + last + 1) / 2, slabSize[axis] = last - first + 1;
353
+ if (from === to || !this.boxBlocked(slab, slabSize, o))
354
+ place[axis] = p[axis];
355
+ else
356
+ {
357
+ // stopped; a fall stopped by a block under it stands on it
358
+ if (axis === 'y' && move < 0)
359
+ o.groundObject = this;
360
+ v[axis] = 0;
361
+ }
362
+ }
363
+ p.set(place.x, place.y, place.z);
364
+ return;
365
+ }
366
+
367
+ // from where it was, each axis alone, y first so a landing wins; a blocked axis goes flush against the block
368
+ // it ran into, or stays where it was, and its speed bounces by the restitution
369
+ const restitution = max(o.restitution, this.restitution);
370
+ const place = oldPos.copy();
371
+ for (const axis of ['y', 'x', 'z'])
372
+ {
373
+ const moved = place.copy();
374
+ moved[axis] = p[axis];
375
+ if (!this.boxBlocked(moved, size, o))
376
+ {
377
+ place[axis] = p[axis];
378
+ continue;
379
+ }
380
+ const half = size[axis] / 2, move = p[axis] - oldPos[axis];
381
+ if (move)
382
+ {
383
+ moved[axis] = move < 0 ? m[axis] + floor(p[axis] - half - m[axis]) + 1 + half + epsilon :
384
+ m[axis] + ceil(p[axis] + half - m[axis]) - 1 - half - epsilon;
385
+ if (!this.boxBlocked(moved, size, o))
386
+ place[axis] = moved[axis];
387
+ }
388
+ if (axis === 'y' && move < 0)
389
+ o.groundObject = this;
390
+ v[axis] *= -restitution;
391
+ }
392
+ p.set(place.x, place.y, place.z);
393
+ }
394
+
395
+ /** The first block a ray hits, walking the grid cell by cell: its distance along the ray, its cell, the normal of
396
+ * the face it comes in through, which a new block goes against, and its type
397
+ * - A ray that starts inside a block hits it at 0, its normal back along the ray
398
+ * @param {Ray3D} ray - Its distance is in the ray's own units, as the other raycasts
399
+ * @param {number} [maxDistance]
400
+ * @param {function(number, Vector3): boolean} [test] - (type, cell) says which blocks count, every block by default
401
+ * @return {VoxelHit|undefined} */
402
+ raycast(ray, maxDistance=Infinity, test=()=> true)
403
+ {
404
+ const s = this.mapSize, o = ray.origin.subtract(this.pos3D), d = ray.direction, axes = ['x', 'y', 'z'];
405
+ if (!d.lengthSquared()) return;
406
+
407
+ // where it enters the map's box, the axis of the face it comes in through, or where it starts inside it
408
+ let enter = 0, exit = maxDistance, entryAxis = -1;
409
+ for (let k = 0; k < 3; ++k)
410
+ {
411
+ const a = axes[k], dk = d[a], ok = o[a];
412
+ if (!dk)
413
+ {
414
+ if (ok < 0 || ok > s[a]) return; // alongside the map, never in it
415
+ continue;
416
+ }
417
+ let t0 = -ok / dk, t1 = (s[a] - ok) / dk;
418
+ if (t0 > t1) [t0, t1] = [t1, t0];
419
+ if (t0 > enter) enter = t0, entryAxis = k;
420
+ exit = min(exit, t1);
421
+ }
422
+ if (enter > exit) return;
423
+
424
+ // walk the cells, stepping across whichever cell edge comes next
425
+ const start = o.add(d.scale(enter)), cell = [], step = [], next = [], delta = [];
426
+ for (let k = 0; k < 3; ++k)
427
+ {
428
+ const a = axes[k], dk = d[a];
429
+ cell[k] = clamp(floor(start[a]), 0, s[a] - 1);
430
+ step[k] = sign(dk);
431
+ next[k] = dk ? enter + (cell[k] + (dk > 0 ? 1 : 0) - start[a]) / dk : Infinity;
432
+ delta[k] = dk ? abs(1 / dk) : Infinity;
433
+ }
434
+ const normalOn = (k)=> vec3(k === 0 ? -step[0] : 0, k === 1 ? -step[1] : 0, k === 2 ? -step[2] : 0);
435
+ let normal, t = enter;
436
+ if (entryAxis >= 0)
437
+ normal = normalOn(entryAxis);
438
+ else
439
+ {
440
+ // starting inside, the normal faces back along the ray's strongest axis
441
+ const k = abs(d.x) >= abs(d.y) && abs(d.x) >= abs(d.z) ? 0 : abs(d.y) >= abs(d.z) ? 1 : 2;
442
+ normal = normalOn(k);
443
+ }
444
+ while (t <= maxDistance)
445
+ {
446
+ const type = this.data[cell[0] + s.x * (cell[1] + s.y * cell[2])];
447
+ if (type)
448
+ {
449
+ const at = vec3(cell[0], cell[1], cell[2]);
450
+ if (test(type, at))
451
+ return {distance: t, cell: at, normal, type};
452
+ }
453
+ const k = next[0] < next[1] ? (next[0] < next[2] ? 0 : 2) : (next[1] < next[2] ? 1 : 2);
454
+ t = next[k];
455
+ cell[k] += step[k];
456
+ if (cell[k] < 0 || cell[k] >= s[axes[k]]) return;
457
+ next[k] += delta[k];
458
+ normal = normalOn(k);
459
+ }
460
+ }
461
+
462
+ /** How far along a ray the first block is, for picking, see raycast
463
+ * @param {Ray3D} ray
464
+ * @return {number|undefined}
465
+ * @ignore */
466
+ levelRaycast3D(ray) { return this.raycast(ray)?.distance; }
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
+
480
+ /** Keeps an eye on its placement, called automatically each frame */
481
+ update()
482
+ {
483
+ super.update();
484
+ render3DLevelAssertPlaced(this);
485
+ }
486
+
487
+ /** Draw the solid and see-through blocks, the transparent ones draw through its child */
488
+ render3D() { this.renderChunks(false); }
489
+
490
+ /** Draw the chunks, each at its center, with the whole texture so each face shows its own tile
491
+ * @param {boolean} transparent
492
+ * @ignore */
493
+ renderChunks(transparent)
494
+ {
495
+ this.buildChunks();
496
+ const meshes = transparent ? this.chunkTransparentMeshes : this.chunkMeshes, p = this.pos3D;
497
+ const texture = this.tileInfo.textureInfo;
498
+ for (let i = 0; i < meshes.length; ++i)
499
+ {
500
+ const mesh = meshes[i], c = this.chunkCenters[i];
501
+ mesh && render3D.drawMesh(mesh, vec3(p.x + c.x, p.y + c.y, p.z + c.z), texture, this.color);
502
+ }
503
+ }
504
+
505
+ /** Destroy the map, it leaves the level's collision and lets go of its meshes
506
+ * @param {boolean} [immediate] */
507
+ destroy(immediate)
508
+ {
509
+ render3DLevelLeave(this);
510
+ for (const mesh of [...this.chunkMeshes, ...this.chunkTransparentMeshes])
511
+ mesh?.dispose();
512
+ super.destroy(immediate);
513
+ }
514
+ }
@@ -0,0 +1,58 @@
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
+ sceneLeaving = true;
42
+ sceneCurrent.leave?.();
43
+ sceneLeaving = false;
44
+ }
45
+ engineObjectsDestroy();
46
+ sceneCurrent = scene;
47
+ scene?.enter?.();
48
+ }
49
+
50
+ /** Get the current scene
51
+ * @return {Scene|undefined}
52
+ * @memberof SceneSystem */
53
+ function getScene() { return sceneCurrent; }
54
+
55
+ // the plugin hooks, update skips the frozen ticks that skip gameUpdate
56
+ function sceneUpdate() { paused || !timeScale || sceneCurrent?.update?.(); }
57
+ function sceneRender() { sceneCurrent?.render?.(); }
58
+ function sceneRenderPost() { sceneCurrent?.renderPost?.(); }