littlejsengine 1.22.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 +633 -94
- package/dist/littlejs.esm.js +2203 -222
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2190 -222
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +716 -76
- package/package.json +1 -1
- package/plugins/lightSystem.js +8 -6
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +37 -22
- package/plugins/pluginExport.js +8 -0
- package/plugins/render3d.js +15 -7
- package/plugins/render3dEditor.js +1021 -111
- package/plugins/render3dExtras.js +82 -21
- package/plugins/render3dLevel.js +300 -6
- package/src/engine.js +1 -1
- package/src/engineAudio.js +10 -2
- package/src/engineBuild.mjs +1 -0
- package/src/engineDraw.js +15 -3
- package/src/engineEditor.js +465 -36
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +48 -6
- package/src/engineRelease.js +13 -2
- package/src/engineSettings.js +14 -0
|
@@ -1058,6 +1058,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1058
1058
|
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
1059
1059
|
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
1060
1060
|
this.collideLevel = false;
|
|
1061
|
+
/** @property {number} - How much a particle grips where it lands, 0 to 1: its speed along the surface is
|
|
1062
|
+
* cut by this much on each hit, on top of the friction, 1 stops it there */
|
|
1063
|
+
this.stick = 0;
|
|
1061
1064
|
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
1062
1065
|
* @type {Particle3DCallback|undefined} */
|
|
1063
1066
|
this.particleCreateCallback = undefined;
|
|
@@ -1077,7 +1080,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1077
1080
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
1078
1081
|
* @type {Vector3|undefined} */
|
|
1079
1082
|
this.worldPos3D = undefined;
|
|
1080
|
-
this.emitTimeBuffer =
|
|
1083
|
+
this.emitTimeBuffer = 1; // the first particle comes at once, as the 2D emitter's does
|
|
1081
1084
|
}
|
|
1082
1085
|
|
|
1083
1086
|
/** Spawn new particles, move the live ones, and go away when done */
|
|
@@ -1094,9 +1097,10 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1094
1097
|
{
|
|
1095
1098
|
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
1096
1099
|
// neither is a reason to stop counting down the emit time
|
|
1097
|
-
|
|
1100
|
+
const rate = this.emitRate * particleEmitRateScale;
|
|
1101
|
+
if (rate > 0 && rate < Infinity)
|
|
1098
1102
|
{
|
|
1099
|
-
this.emitTimeBuffer +=
|
|
1103
|
+
this.emitTimeBuffer += rate * timeDelta;
|
|
1100
1104
|
for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
|
|
1101
1105
|
this.emitParticle();
|
|
1102
1106
|
}
|
|
@@ -1136,7 +1140,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1136
1140
|
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
1137
1141
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
1138
1142
|
data[k+18] += data[k+19] *= angleDamping;
|
|
1139
|
-
|
|
1143
|
+
// true when its collide callback destroyed it, it has had its last callback but the destroy one
|
|
1144
|
+
const gone = collideLevel && this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz);
|
|
1145
|
+
collideLevel && reread();
|
|
1140
1146
|
const t = i * trailMax * 3;
|
|
1141
1147
|
if (trailMax)
|
|
1142
1148
|
{
|
|
@@ -1148,7 +1154,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1148
1154
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
1149
1155
|
data[k+20] = n + 1;
|
|
1150
1156
|
}
|
|
1151
|
-
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
1157
|
+
updateCallback && !gone && (this.particleCall(updateCallback, k), reread());
|
|
1152
1158
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
1153
1159
|
{
|
|
1154
1160
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
@@ -1250,13 +1256,14 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1250
1256
|
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
1251
1257
|
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
1252
1258
|
if (view.destroyed)
|
|
1253
|
-
out[k+17] = max(out[k+17], out[k+16]
|
|
1259
|
+
out[k+17] = max(out[k+17], out[k+16]); // its life lived: a step short of it can round to just under
|
|
1254
1260
|
return result;
|
|
1255
1261
|
}
|
|
1256
1262
|
|
|
1257
1263
|
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
1258
1264
|
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
1259
|
-
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
1265
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is; true when the callback
|
|
1266
|
+
// destroyed it
|
|
1260
1267
|
particleCollide(k, x, y, z)
|
|
1261
1268
|
{
|
|
1262
1269
|
let data = this.particleData;
|
|
@@ -1269,18 +1276,21 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1269
1276
|
if (h && (!hit || h.distance < hit.distance))
|
|
1270
1277
|
hit = h, level = l;
|
|
1271
1278
|
}
|
|
1272
|
-
if (!hit) return;
|
|
1279
|
+
if (!hit) return false;
|
|
1273
1280
|
const point = from.lerp(to, hit.distance);
|
|
1274
1281
|
const callback = this.particleCollideCallback;
|
|
1275
1282
|
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
1276
|
-
return; // let through, or destroyed by the callback
|
|
1283
|
+
return this.particleView.destroyed; // let through, or destroyed by the callback
|
|
1277
1284
|
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
1278
1285
|
|
|
1279
1286
|
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
1280
|
-
|
|
1287
|
+
// the larger friction of the two, as objects take it, then the emitter's own grip, which a level can not undo
|
|
1288
|
+
const restitution = max(this.restitution, level.restitution);
|
|
1289
|
+
const friction = max(this.friction, level.friction) * (1 - clamp(this.stick));
|
|
1281
1290
|
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
1282
1291
|
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
1283
1292
|
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
1293
|
+
return false;
|
|
1284
1294
|
}
|
|
1285
1295
|
|
|
1286
1296
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -1544,6 +1554,11 @@ class LensFlare3D extends EngineObject3D
|
|
|
1544
1554
|
* height; shape is glow, disc or ring
|
|
1545
1555
|
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
1546
1556
|
this.elements = undefined;
|
|
1557
|
+
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
1558
|
+
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
1559
|
+
* in front of the light, and a spotlight's shows from inside its beam only
|
|
1560
|
+
* @type {Light3D|undefined} */
|
|
1561
|
+
this.light = undefined;
|
|
1547
1562
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
1548
1563
|
this.occlusion = true;
|
|
1549
1564
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
@@ -1582,13 +1597,30 @@ class LensFlare3D extends EngineObject3D
|
|
|
1582
1597
|
return this.made;
|
|
1583
1598
|
}
|
|
1584
1599
|
|
|
1585
|
-
|
|
1600
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun, and a point
|
|
1601
|
+
// to find it on the screen by; undefined with no sun direction, or a light that is gone or at the camera
|
|
1602
|
+
flareSource()
|
|
1603
|
+
{
|
|
1604
|
+
const camera = render3D.camera.pos, light = this.light;
|
|
1605
|
+
if (!light)
|
|
1606
|
+
{
|
|
1607
|
+
const sun = render3D.sunDirection;
|
|
1608
|
+
if (!sun.lengthSquared()) return;
|
|
1609
|
+
const direction = sun.normalize();
|
|
1610
|
+
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
1611
|
+
}
|
|
1612
|
+
if (light.destroyed) return;
|
|
1613
|
+
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
1614
|
+
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
1618
|
+
* behind the camera
|
|
1586
1619
|
* @return {Vector2|undefined} */
|
|
1587
1620
|
getSunScreenPos()
|
|
1588
1621
|
{
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
return render3D.worldToScreen(render3D.camera.pos.add(direction.normalize(100)));
|
|
1622
|
+
const source = this.flareSource();
|
|
1623
|
+
return source && render3D.worldToScreen(source.pos);
|
|
1592
1624
|
}
|
|
1593
1625
|
|
|
1594
1626
|
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
@@ -1602,7 +1634,19 @@ class LensFlare3D extends EngineObject3D
|
|
|
1602
1634
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
1603
1635
|
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
1604
1636
|
if (!(strength > 0)) return [];
|
|
1605
|
-
|
|
1637
|
+
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
1638
|
+
const light = this.light, source = light && this.flareSource();
|
|
1639
|
+
if (light)
|
|
1640
|
+
{
|
|
1641
|
+
if (!source) return [];
|
|
1642
|
+
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
1643
|
+
const cone = render3DLightCone(light), d = source.direction;
|
|
1644
|
+
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
1645
|
+
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
1646
|
+
if (!(bright > 0)) return [];
|
|
1647
|
+
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
1648
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
1649
|
+
}
|
|
1606
1650
|
return this.getElements().map((e)=>
|
|
1607
1651
|
{
|
|
1608
1652
|
const c = e.color.multiply(tint);
|
|
@@ -1612,15 +1656,32 @@ class LensFlare3D extends EngineObject3D
|
|
|
1612
1656
|
});
|
|
1613
1657
|
}
|
|
1614
1658
|
|
|
1615
|
-
/** Is something between the camera and the sun: the level, or an object that is not see
|
|
1659
|
+
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
1660
|
+
* through
|
|
1616
1661
|
* @return {boolean} */
|
|
1617
1662
|
isSunHidden()
|
|
1618
1663
|
{
|
|
1619
|
-
const
|
|
1620
|
-
if (!
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1664
|
+
const source = this.flareSource();
|
|
1665
|
+
if (!source) return true;
|
|
1666
|
+
const ray = new Ray3D(render3D.camera.pos, source.direction), reach = source.distance;
|
|
1667
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.destroyed &&
|
|
1668
|
+
!o.transparent && !o.additive);
|
|
1669
|
+
// a voxel map says block by block what is see through: glass, water and leaves let the sun by, and the
|
|
1670
|
+
// ray goes on to the blocks behind them
|
|
1671
|
+
const maps = /** @type {Array<VoxelMap>} */ (typeof VoxelMap == 'undefined' ? [] :
|
|
1672
|
+
blockers.filter((o)=> o instanceof VoxelMap));
|
|
1673
|
+
for (const map of maps)
|
|
1674
|
+
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
1675
|
+
return true;
|
|
1676
|
+
// an object is hit by its box, and a box the camera is inside, a room, a wide floor or the player's own
|
|
1677
|
+
// body, says nothing of what is in the way: the ray the other way hits it too, and it is left out; so is
|
|
1678
|
+
// one the light is inside, its lamp
|
|
1679
|
+
const backward = ray.direction.scale(-1), back = new Ray3D(ray.origin, backward);
|
|
1680
|
+
const lamp = this.light && [new Ray3D(source.pos, ray.direction), new Ray3D(source.pos, backward)];
|
|
1681
|
+
const hits = (r, o)=> render3DRaycastObject(r, o) !== undefined;
|
|
1682
|
+
const around = (o)=> !(o instanceof HeightMap) && (hits(back, o) || lamp && hits(lamp[0], o) && hits(lamp[1], o));
|
|
1683
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !around(o)));
|
|
1684
|
+
return !!hit && hit.distance < reach;
|
|
1624
1685
|
}
|
|
1625
1686
|
|
|
1626
1687
|
/** Ease visible toward whether the sun shows, called automatically each frame */
|
package/plugins/render3dLevel.js
CHANGED
|
@@ -78,7 +78,10 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
78
78
|
* returned, see level3DVoxelSetup for its sheet
|
|
79
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
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
|
|
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
|
|
82
85
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
83
86
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
84
87
|
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
|
|
@@ -91,6 +94,7 @@ function level3DLoad(level)
|
|
|
91
94
|
ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
92
95
|
ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
93
96
|
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
97
|
+
level3DPrefabsAdd(level.prefabs);
|
|
94
98
|
level3DSceneApply(level.scene);
|
|
95
99
|
const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
|
|
96
100
|
map && made.push(map);
|
|
@@ -201,14 +205,49 @@ function level3DTerrainShape(terrain)
|
|
|
201
205
|
height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
202
206
|
}
|
|
203
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
|
+
|
|
204
243
|
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
205
244
|
function level3DTerrainMake(terrain)
|
|
206
245
|
{
|
|
207
246
|
const shape = level3DTerrainShape(terrain);
|
|
208
247
|
if (!shape || typeof HeightMap == 'undefined') return;
|
|
209
248
|
const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
|
|
210
|
-
|
|
211
|
-
|
|
249
|
+
level3DTerrainSetColors(map, terrain);
|
|
250
|
+
map.colors && map.rebuild();
|
|
212
251
|
return map;
|
|
213
252
|
}
|
|
214
253
|
|
|
@@ -281,6 +320,14 @@ function level3DProperties(type, object)
|
|
|
281
320
|
// make one object of a level from the type added for its name, undefined when there is no such type or it made
|
|
282
321
|
// nothing
|
|
283
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)
|
|
284
331
|
{
|
|
285
332
|
const type = level3DTypes.get(object.type);
|
|
286
333
|
if (!type)
|
|
@@ -288,7 +335,7 @@ function level3DMake(object)
|
|
|
288
335
|
debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
|
|
289
336
|
return;
|
|
290
337
|
}
|
|
291
|
-
const
|
|
338
|
+
const properties = level3DProperties(type, object);
|
|
292
339
|
const {make} = type, count = engineObjects.length;
|
|
293
340
|
let result;
|
|
294
341
|
try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
|
|
@@ -306,16 +353,263 @@ function level3DMake(object)
|
|
|
306
353
|
if (result instanceof EngineObject3D)
|
|
307
354
|
{
|
|
308
355
|
level3DBaseScale.set(result, result.scale3D.copy());
|
|
309
|
-
result.rotation3D =
|
|
310
|
-
result.scale3D = result.scale3D.multiply(
|
|
356
|
+
result.rotation3D = rotation.copy();
|
|
357
|
+
result.scale3D = result.scale3D.multiply(scale);
|
|
311
358
|
}
|
|
312
359
|
|
|
313
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
|
|
314
361
|
for (const key in type.defaults)
|
|
315
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);
|
|
316
365
|
return result;
|
|
317
366
|
}
|
|
318
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
|
+
|
|
319
613
|
///////////////////////////////////////////////////////////////////////////////
|
|
320
614
|
// the built-in types
|
|
321
615
|
|
package/src/engine.js
CHANGED
package/src/engineAudio.js
CHANGED
|
@@ -233,10 +233,9 @@ class Sound
|
|
|
233
233
|
ASSERT(isNumber(range), 'range must be a number');
|
|
234
234
|
ASSERT(isNumber(taper), 'taper must be a number');
|
|
235
235
|
|
|
236
|
-
/** @property {number} - World space max range of sound */
|
|
237
236
|
this.range = range;
|
|
238
237
|
/** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
|
|
239
|
-
* to render3D.soundDefaultRange instead, which is further */
|
|
238
|
+
* to render3D.soundDefaultRange instead, which is further; setting range makes it false */
|
|
240
239
|
this.rangeIsDefault = rangeIsDefault;
|
|
241
240
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
242
241
|
this.taper = taper;
|
|
@@ -292,6 +291,15 @@ class Sound
|
|
|
292
291
|
}
|
|
293
292
|
}
|
|
294
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
|
+
|
|
295
303
|
/** Sample data for each channel
|
|
296
304
|
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
297
305
|
* the arrays from it and caches them. The copies are safe to hold onto,
|
package/src/engineBuild.mjs
CHANGED
|
@@ -50,6 +50,7 @@ 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`,
|
package/src/engineDraw.js
CHANGED
|
@@ -496,17 +496,29 @@ class SpriteAnimator
|
|
|
496
496
|
* @return {SpriteAnimation} */
|
|
497
497
|
get clip() { return this.clips[this.name]; }
|
|
498
498
|
|
|
499
|
+
/** The clip showing now, once a clip that has ended has called its onEnd: the callback may switch clips, and a
|
|
500
|
+
* read gives the clip it switched to, not the one that ended
|
|
501
|
+
* @return {SpriteAnimation}
|
|
502
|
+
* @ignore */
|
|
503
|
+
get settledClip()
|
|
504
|
+
{
|
|
505
|
+
// each clip's own isDone calls its onEnd once; a few switches in a row at most
|
|
506
|
+
for (let i = 4, clip; i-- && clip !== this.clip;)
|
|
507
|
+
(clip = this.clip).isDone;
|
|
508
|
+
return this.clip;
|
|
509
|
+
}
|
|
510
|
+
|
|
499
511
|
/** The tile of the frame showing now
|
|
500
512
|
* @return {TileInfo} */
|
|
501
|
-
get tileInfo() { return this.
|
|
513
|
+
get tileInfo() { return this.settledClip.tileInfo; }
|
|
502
514
|
|
|
503
515
|
/** The frame of the clip showing now
|
|
504
516
|
* @return {number} */
|
|
505
|
-
get frame() { return this.
|
|
517
|
+
get frame() { return this.settledClip.frame; }
|
|
506
518
|
|
|
507
519
|
/** True once the clip showing now is a play that has ended
|
|
508
520
|
* @return {boolean} */
|
|
509
|
-
get isDone() { return this.
|
|
521
|
+
get isDone() { return this.settledClip.isDone; }
|
|
510
522
|
}
|
|
511
523
|
|
|
512
524
|
///////////////////////////////////////////////////////////////////////////////
|