littlejsengine 1.20.0 → 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.
- package/dist/littlejs.d.ts +408 -26
- package/dist/littlejs.esm.js +3360 -161
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3344 -161
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1448 -141
- package/package.json +1 -1
- package/plugins/gltf.js +54 -8
- package/plugins/math3d.js +90 -10
- package/plugins/pluginExport.js +10 -0
- package/plugins/postProcess.js +41 -12
- package/plugins/render3d.js +424 -55
- package/plugins/render3dDebug.js +2 -1
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +199 -5
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +12 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/tweenSystem.js +4 -2
- package/src/engine.js +199 -34
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +3 -0
- package/src/engineDebug.js +55 -1
- package/src/engineDraw.js +97 -6
- package/src/engineEditor.js +41 -20
- package/src/engineExport.js +6 -0
- package/src/engineInput.js +8 -4
- package/src/engineLogo.js +19 -0
- package/src/engineObject.js +3 -2
- package/src/engineRelease.js +13 -1
- package/src/engineSettings.js +38 -0
|
@@ -38,6 +38,54 @@ function render3DSoftDot()
|
|
|
38
38
|
return render3DSoftDotTexture = new TextureInfo(context.canvas);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Make a normal map from a height at each pixel, for bumps and grooves with no image file: the slope at each pixel
|
|
42
|
+
* from its neighbors' heights, taken around the edges so the map tiles; set it as an object's normalMap
|
|
43
|
+
* @param {Vector2} size - In pixels
|
|
44
|
+
* @param {function(number, number): number} heightFunction - The height 0 to 1 at a pixel, x across and y down
|
|
45
|
+
* @param {number} [strength] - How steep the slopes are: a height change of 1 over one pixel leans the normal
|
|
46
|
+
* by strength
|
|
47
|
+
* @return {TextureInfo} - Wraps; headless it has no image
|
|
48
|
+
* @memberof Render3D */
|
|
49
|
+
function normalMapFromHeight(size, heightFunction, strength=1)
|
|
50
|
+
{
|
|
51
|
+
ASSERT(isVector2(size) && size.x >= 1 && size.y >= 1, 'normalMapFromHeight size must be a Vector2 of pixels');
|
|
52
|
+
const width = size.x | 0, height = size.y | 0;
|
|
53
|
+
if (typeof OffscreenCanvas == 'undefined')
|
|
54
|
+
{
|
|
55
|
+
// headless, nothing to draw into; the right size so what reads it still works
|
|
56
|
+
const textureInfo = new TextureInfo(undefined, false, true);
|
|
57
|
+
textureInfo.size = vec2(width, height), textureInfo.sizeInverse = vec2(1 / width, 1 / height);
|
|
58
|
+
return textureInfo;
|
|
59
|
+
}
|
|
60
|
+
const context = createCanvasContext(width, height);
|
|
61
|
+
const pixels = render3DNormalMapPixels(width, height, heightFunction, strength);
|
|
62
|
+
context.putImageData(new ImageData(pixels, width, height), 0, 0);
|
|
63
|
+
return new TextureInfo(context.canvas, true, true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// the pixels of a normal map made from heights, rgba with the top row first: each normal leans away from the uphill
|
|
67
|
+
// side, green up the image as OpenGL has it, and a flat height is (128, 128, 255); rounded before they are stored,
|
|
68
|
+
// since the clamped array would round a flat 127.5 to even
|
|
69
|
+
function render3DNormalMapPixels(width, height, heightFunction, strength)
|
|
70
|
+
{
|
|
71
|
+
const heights = new Float32Array(width * height), pixels = new Uint8ClampedArray(width * height * 4);
|
|
72
|
+
for (let y = 0; y < height; ++y)
|
|
73
|
+
for (let x = 0; x < width; ++x)
|
|
74
|
+
heights[y * width + x] = heightFunction(x, y);
|
|
75
|
+
const h = (x, y)=> heights[mod(y, height) * width + mod(x, width)];
|
|
76
|
+
for (let y = 0; y < height; ++y)
|
|
77
|
+
for (let x = 0; x < width; ++x)
|
|
78
|
+
{
|
|
79
|
+
const dx = (h(x+1, y) - h(x-1, y)) / 2 * strength, dy = (h(x, y+1) - h(x, y-1)) / 2 * strength;
|
|
80
|
+
const k = 1 / hypot(dx, dy, 1), i = (y * width + x) * 4;
|
|
81
|
+
pixels[i] = round((-dx * k * .5 + .5) * 255);
|
|
82
|
+
pixels[i+1] = round((dy * k * .5 + .5) * 255);
|
|
83
|
+
pixels[i+2] = round((k * .5 + .5) * 255);
|
|
84
|
+
pixels[i+3] = 255;
|
|
85
|
+
}
|
|
86
|
+
return pixels;
|
|
87
|
+
}
|
|
88
|
+
|
|
41
89
|
///////////////////////////////////////////////////////////////////////////////
|
|
42
90
|
// The rest of the shape builders: buildLathe, buildSphere, buildBox, buildGrid and buildSky live with the
|
|
43
91
|
// renderer, since it hands those out itself
|
|
@@ -574,6 +622,22 @@ class HeightMap extends EngineObject3D
|
|
|
574
622
|
* @ignore */
|
|
575
623
|
levelRaycast3D(ray) { return this.raycast(ray); }
|
|
576
624
|
|
|
625
|
+
/** Where a short move goes under the surface, for a particle's move in one frame: the part of the move made
|
|
626
|
+
* before it, 0 to 1, and the surface normal there; undefined when it stays above, starts under, or is off the map
|
|
627
|
+
* @param {Vector3} from
|
|
628
|
+
* @param {Vector3} to
|
|
629
|
+
* @return {{distance: number, normal: Vector3}|undefined}
|
|
630
|
+
* @ignore */
|
|
631
|
+
levelSegment3D(from, to)
|
|
632
|
+
{
|
|
633
|
+
const m = this.pos3D, size = this.mapSize;
|
|
634
|
+
if (abs(to.x - m.x) > size.x / 2 || abs(to.z - m.z) > size.y / 2) return; // off the map
|
|
635
|
+
const a = from.y - this.getHeight(from.x, from.z), b = to.y - this.getHeight(to.x, to.z);
|
|
636
|
+
if (a < 0 || b >= 0) return; // above all the way, or under from the start
|
|
637
|
+
const distance = a / (a - b);
|
|
638
|
+
return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
|
|
639
|
+
}
|
|
640
|
+
|
|
577
641
|
/** Keep an object above the ground, called by the engine for each object with collideLevel
|
|
578
642
|
* @param {EngineObject3D} o
|
|
579
643
|
* @param {Vector3} oldPos - Where it was before it moved
|
|
@@ -762,7 +826,7 @@ class FirstPersonCamera3D extends EngineObject3D
|
|
|
762
826
|
/** @property {boolean} - Capture the mouse on a click, so looking needs no button held */
|
|
763
827
|
this.lockPointer = true;
|
|
764
828
|
/** @property {number} - Speed of a jump in world units a frame, 0 for none; Space or gamepad button 0 jumps
|
|
765
|
-
* while it stands on something, a height map
|
|
829
|
+
* while it stands on something, a height map, a voxel map or a solid, see groundObject */
|
|
766
830
|
this.jumpSpeed = 0;
|
|
767
831
|
/** @property {number} - How far above its position the eye is, in its own space, so with a size3D the eye can
|
|
768
832
|
* sit toward the top of the body instead of its middle; keep it under half the body's height, or the eye is
|
|
@@ -833,6 +897,49 @@ class FirstPersonCamera3D extends EngineObject3D
|
|
|
833
897
|
const RENDER3D_PARTICLE_FLOATS = 21;
|
|
834
898
|
// the position, color and size of the particle being drawn, shared by every emitter so the draw loop makes no objects
|
|
835
899
|
const render3DParticlePos = vec3(), render3DParticleColor = new Color, render3DParticleSize = vec2();
|
|
900
|
+
// where a particle was and is, for the level collision
|
|
901
|
+
const render3DParticleFrom = vec3(), render3DParticleTo = vec3();
|
|
902
|
+
|
|
903
|
+
// the emitters inside a particle callback now, whose nested calls each get a view of their own
|
|
904
|
+
const render3DParticlesCalling = new Set;
|
|
905
|
+
|
|
906
|
+
// a particle as a ParticleEmitter3D's callbacks see it, set from the particle for each call
|
|
907
|
+
/** @param {ParticleEmitter3D} emitter
|
|
908
|
+
* @return {Particle3D} */
|
|
909
|
+
function render3DParticleView(emitter)
|
|
910
|
+
{
|
|
911
|
+
return {emitter, pos: vec3(), velocity: vec3(), age: 0, lifeTime: 0, destroyed: false,
|
|
912
|
+
destroy() { this.destroyed = true; }};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* A particle as a ParticleEmitter3D's callbacks see it: one object the emitter reuses, set from the particle for each
|
|
917
|
+
* call and written back after it, so copy what you keep
|
|
918
|
+
* @typedef {Object} Particle3D
|
|
919
|
+
* @property {ParticleEmitter3D} emitter - The emitter it is in
|
|
920
|
+
* @property {Vector3} pos - Where it is, change it to move it
|
|
921
|
+
* @property {Vector3} velocity - How far it moves each frame
|
|
922
|
+
* @property {number} age - Seconds it has lived
|
|
923
|
+
* @property {number} lifeTime - Seconds it lives
|
|
924
|
+
* @property {boolean} destroyed - Set by destroy
|
|
925
|
+
* @property {function(): void} destroy - End it this update, the destroy callback gets it
|
|
926
|
+
* @memberof Render3D
|
|
927
|
+
*/
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* @callback Particle3DCallback - A function a ParticleEmitter3D calls with one of its particles
|
|
931
|
+
* @param {Particle3D} particle
|
|
932
|
+
* @memberof Render3D
|
|
933
|
+
*/
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* @callback Particle3DCollideCallback - Decides whether a particle stops where it hits the level, a filter as in 2D
|
|
937
|
+
* @param {Particle3D} particle
|
|
938
|
+
* @param {EngineObject3D} level - The HeightMap or VoxelMap it hit
|
|
939
|
+
* @param {Vector3} pos - Where it hit
|
|
940
|
+
* @return {boolean|void} - true to stop it there; a callback that returns nothing lets it pass through
|
|
941
|
+
* @memberof Render3D
|
|
942
|
+
*/
|
|
836
943
|
|
|
837
944
|
/**
|
|
838
945
|
* ParticleEmitter3D - Spawns camera facing particles, the 3D twin of ParticleEmitter
|
|
@@ -846,6 +953,10 @@ const render3DParticlePos = vec3(), render3DParticleColor = new Color, render3DP
|
|
|
846
953
|
* - gravity here is its own number added to velocity y each frame: it is neither the engine's 2D
|
|
847
954
|
* gravity nor render3D.gravity, so an effect keeps its own fall wherever it is used
|
|
848
955
|
* - An emitter with an emitTime destroys itself once its last particle is gone, like the 2D emitter
|
|
956
|
+
* - Callbacks as the 2D emitter's: particleCreateCallback, particleUpdateCallback, particleCollideCallback and
|
|
957
|
+
* particleDestroyCallback, each given a Particle3D, one object the emitter reuses for every particle and call
|
|
958
|
+
* - collideLevel, off by default, has particles hit the height maps and voxel maps, bouncing by restitution and
|
|
959
|
+
* sliding by friction
|
|
849
960
|
* @extends EngineObject3D
|
|
850
961
|
* @memberof Render3D
|
|
851
962
|
* @example
|
|
@@ -918,7 +1029,8 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
918
1029
|
this.randomness = randomness;
|
|
919
1030
|
/** @property {boolean} - Additive blending */
|
|
920
1031
|
this.additive = additive;
|
|
921
|
-
/** @property {number} - Seconds of each particle's path to draw as a ribbon behind it, 0 draws billboards
|
|
1032
|
+
/** @property {number} - Seconds of each particle's path to draw as a ribbon behind it, 0 draws billboards;
|
|
1033
|
+
* with engineVariableStep it keeps trailTime * frameRate updates of path */
|
|
922
1034
|
this.trailTime = 0;
|
|
923
1035
|
/** @property {number} - Radians per frame each particle turns in the camera plane, either way; 0 is no spin */
|
|
924
1036
|
this.angleSpeed = 0;
|
|
@@ -934,6 +1046,25 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
934
1046
|
this.trailData = undefined;
|
|
935
1047
|
/** @property {number} - Trail points kept per particle, from trailTime */
|
|
936
1048
|
this.trailMax = 0;
|
|
1049
|
+
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
1050
|
+
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
1051
|
+
this.collideLevel = false;
|
|
1052
|
+
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
1053
|
+
* @type {Particle3DCallback|undefined} */
|
|
1054
|
+
this.particleCreateCallback = undefined;
|
|
1055
|
+
/** @property {Particle3DCallback|undefined} - Called with each particle each update, after it moves
|
|
1056
|
+
* @type {Particle3DCallback|undefined} */
|
|
1057
|
+
this.particleUpdateCallback = undefined;
|
|
1058
|
+
/** @property {Particle3DCollideCallback|undefined} - Decides if a particle stops where it hits the level,
|
|
1059
|
+
* with collideLevel on; a callback that returns nothing lets it through
|
|
1060
|
+
* @type {Particle3DCollideCallback|undefined} */
|
|
1061
|
+
this.particleCollideCallback = undefined;
|
|
1062
|
+
/** @property {Particle3DCallback|undefined} - Called with each particle as it goes, its life over or destroyed
|
|
1063
|
+
* @type {Particle3DCallback|undefined} */
|
|
1064
|
+
this.particleDestroyCallback = undefined;
|
|
1065
|
+
/** @property {Particle3D} - The particle the callbacks get, one object for every particle and call
|
|
1066
|
+
* @type {Particle3D} */
|
|
1067
|
+
this.particleView = render3DParticleView(this);
|
|
937
1068
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
938
1069
|
* @type {Vector3|undefined} */
|
|
939
1070
|
this.worldPos3D = undefined;
|
|
@@ -963,8 +1094,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
963
1094
|
else if (!this.particleCount)
|
|
964
1095
|
this.destroy();
|
|
965
1096
|
|
|
966
|
-
// the trail storage follows trailTime, a change starts every trail over
|
|
967
|
-
|
|
1097
|
+
// the trail storage follows trailTime at the fixed rate, a change starts every trail over, so a varying
|
|
1098
|
+
// timeDelta can not; with engineVariableStep a trail keeps that many updates
|
|
1099
|
+
const trailMax = this.trailTime ? max(1, round(this.trailTime * frameRate)) : 0;
|
|
968
1100
|
if (trailMax !== this.trailMax)
|
|
969
1101
|
{
|
|
970
1102
|
this.trailMax = trailMax;
|
|
@@ -974,8 +1106,13 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
974
1106
|
}
|
|
975
1107
|
|
|
976
1108
|
// move the particles and drop the dead ones, all in the typed array: this runs for every particle every frame
|
|
977
|
-
|
|
1109
|
+
// a callback can emit, which grows the arrays when they are full, so they are read again after each one
|
|
1110
|
+
const F = RENDER3D_PARTICLE_FLOATS;
|
|
1111
|
+
let data = this.particleData, trail = this.trailData;
|
|
1112
|
+
const reread = ()=> { data = this.particleData, trail = this.trailData; };
|
|
978
1113
|
const damping = this.damping, gravity = this.gravity * scale, angleDamping = this.angleDamping; // a bigger effect has to fall faster to keep the same arc
|
|
1114
|
+
const collideLevel = this.collideLevel && render3DLevel.length;
|
|
1115
|
+
const updateCallback = this.particleUpdateCallback, destroyCallback = this.particleDestroyCallback;
|
|
979
1116
|
for (let i = this.particleCount; i--;)
|
|
980
1117
|
{
|
|
981
1118
|
// damped first and gravity added after, the order the 2D particle uses, so the same
|
|
@@ -984,6 +1121,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
984
1121
|
const vx = data[k+3] *= damping, vy = data[k+4] = data[k+4] * damping + gravity, vz = data[k+5] *= damping;
|
|
985
1122
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
986
1123
|
data[k+18] += data[k+19] *= angleDamping;
|
|
1124
|
+
collideLevel && (this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz), reread());
|
|
987
1125
|
const t = i * trailMax * 3;
|
|
988
1126
|
if (trailMax)
|
|
989
1127
|
{
|
|
@@ -995,9 +1133,11 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
995
1133
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
996
1134
|
data[k+20] = n + 1;
|
|
997
1135
|
}
|
|
1136
|
+
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
998
1137
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
999
1138
|
{
|
|
1000
1139
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
1140
|
+
destroyCallback && (this.particleCall(destroyCallback, k), reread());
|
|
1001
1141
|
const last = --this.particleCount, kl = last * F;
|
|
1002
1142
|
if (i !== last)
|
|
1003
1143
|
{
|
|
@@ -1064,6 +1204,60 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1064
1204
|
data[k+18] = this.angleSpeed ? rand(2*PI) : 0;
|
|
1065
1205
|
data[k+19] = this.angleSpeed ? this.angleSpeed * random() * randSign() : 0;
|
|
1066
1206
|
data[k+20] = 0; // trail points
|
|
1207
|
+
this.particleCreateCallback && this.particleCall(this.particleCreateCallback, k);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// call a particle callback with the particle at k: the emitter's one Particle3D is set from its numbers, and
|
|
1211
|
+
// what the callback changes is written back; a particle it destroys has lived its life, the update removes it
|
|
1212
|
+
particleCall(callback, k, level, pos)
|
|
1213
|
+
{
|
|
1214
|
+
// a callback that emits runs the create callback inside itself, which gets a view of its own so this one
|
|
1215
|
+
// keeps its particle; what the callback changes is written to the arrays as they are after it, since an
|
|
1216
|
+
// emit into a full emitter grows them
|
|
1217
|
+
const nested = render3DParticlesCalling.has(this);
|
|
1218
|
+
const view = nested ? render3DParticleView(this) : this.particleView, data = this.particleData;
|
|
1219
|
+
view.pos.set(data[k], data[k+1], data[k+2]);
|
|
1220
|
+
view.velocity.set(data[k+3], data[k+4], data[k+5]);
|
|
1221
|
+
view.age = data[k+17], view.lifeTime = data[k+16], view.destroyed = false;
|
|
1222
|
+
render3DParticlesCalling.add(this);
|
|
1223
|
+
let result;
|
|
1224
|
+
try { result = callback(view, level, pos); }
|
|
1225
|
+
finally { nested || render3DParticlesCalling.delete(this); }
|
|
1226
|
+
const out = this.particleData;
|
|
1227
|
+
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
1228
|
+
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
1229
|
+
if (view.destroyed)
|
|
1230
|
+
out[k+17] = max(out[k+17], out[k+16] - timeDelta); // aged past its life this update
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
1235
|
+
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
1236
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
1237
|
+
particleCollide(k, x, y, z)
|
|
1238
|
+
{
|
|
1239
|
+
let data = this.particleData;
|
|
1240
|
+
const from = render3DParticleFrom.set(x, y, z);
|
|
1241
|
+
const to = render3DParticleTo.set(data[k], data[k+1], data[k+2]);
|
|
1242
|
+
let hit, level;
|
|
1243
|
+
for (const l of render3DLevel)
|
|
1244
|
+
{
|
|
1245
|
+
const h = l.destroyed ? undefined : l.levelSegment3D(from, to);
|
|
1246
|
+
if (h && (!hit || h.distance < hit.distance))
|
|
1247
|
+
hit = h, level = l;
|
|
1248
|
+
}
|
|
1249
|
+
if (!hit) return;
|
|
1250
|
+
const point = from.lerp(to, hit.distance);
|
|
1251
|
+
const callback = this.particleCollideCallback;
|
|
1252
|
+
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
1253
|
+
return; // let through, or destroyed by the callback
|
|
1254
|
+
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
1255
|
+
|
|
1256
|
+
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
1257
|
+
const restitution = max(this.restitution, level.restitution), friction = max(this.friction, level.friction);
|
|
1258
|
+
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
1259
|
+
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
1260
|
+
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
1067
1261
|
}
|
|
1068
1262
|
|
|
1069
1263
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
* @param {Object} level - The level, the level editor edits this same object
|
|
77
|
+
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
78
|
+
* @memberof Level3D */
|
|
79
|
+
function level3DLoad(level)
|
|
80
|
+
{
|
|
81
|
+
ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
82
|
+
ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
83
|
+
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
84
|
+
const made = [];
|
|
85
|
+
for (const object of isArray(level.objects) ? level.objects : [])
|
|
86
|
+
{
|
|
87
|
+
if (!object || typeof object !== 'object') continue;
|
|
88
|
+
const result = level3DMake(object);
|
|
89
|
+
editor3DObjectMade(object, result); // debug builds link it for the 3D editor
|
|
90
|
+
result && made.push(result);
|
|
91
|
+
}
|
|
92
|
+
return made;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
96
|
+
function level3DVector(value, fallback)
|
|
97
|
+
{
|
|
98
|
+
return isArray(value) && value.length === 3 && value.every((v)=> isNumber(v)) ?
|
|
99
|
+
vec3(value[0], value[1], value[2]) : fallback;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// the defaults of an object's type, a Color or vector copied for each object, then its own properties over them,
|
|
103
|
+
// each read as its default is; one the file got wrong, a value not of its default's type, keeps the default, and
|
|
104
|
+
// one the type has no default for is kept as it is
|
|
105
|
+
function level3DProperties(type, object)
|
|
106
|
+
{
|
|
107
|
+
const properties = {}, own = object.properties;
|
|
108
|
+
for (const [key, value] of Object.entries(type.defaults))
|
|
109
|
+
properties[key] = value?.copy ? value.copy() : value;
|
|
110
|
+
for (const [key, value] of Object.entries(own && typeof own === 'object' ? own : {}))
|
|
111
|
+
{
|
|
112
|
+
const d = type.defaults[key];
|
|
113
|
+
if (isColor(d))
|
|
114
|
+
/^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) && (properties[key] = new Color().setHex(value));
|
|
115
|
+
else if (isVector3(d))
|
|
116
|
+
properties[key] = level3DVector(value, properties[key]);
|
|
117
|
+
else if (isVector2(d))
|
|
118
|
+
isArray(value) && value.length === 2 && value.every((v)=> isNumber(v)) &&
|
|
119
|
+
(properties[key] = vec2(value[0], value[1]));
|
|
120
|
+
else if (d === undefined || typeof value === typeof d)
|
|
121
|
+
properties[key] = value;
|
|
122
|
+
}
|
|
123
|
+
return properties;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// make one object of a level from the type added for its name, undefined when there is no such type or it made
|
|
127
|
+
// nothing
|
|
128
|
+
function level3DMake(object)
|
|
129
|
+
{
|
|
130
|
+
const type = level3DTypes.get(object.type);
|
|
131
|
+
if (!type)
|
|
132
|
+
{
|
|
133
|
+
debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const pos = level3DVector(object.pos, vec3()), properties = level3DProperties(type, object);
|
|
137
|
+
const {make} = type, count = engineObjects.length;
|
|
138
|
+
let result;
|
|
139
|
+
try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
|
|
140
|
+
catch (error)
|
|
141
|
+
{
|
|
142
|
+
// a value the type can not be made with: in debug builds, where asserts throw, the object is skipped with
|
|
143
|
+
// what it made so far, so the rest of the level loads and the level editor can put the value right
|
|
144
|
+
if (!debug) throw error;
|
|
145
|
+
console.error(`level3DLoad: ${object.type} ${object.id} could not be made, skipped:`, error);
|
|
146
|
+
for (const o of engineObjects.slice(count))
|
|
147
|
+
o.destroy();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!result || typeof result !== 'object') return;
|
|
151
|
+
if (result instanceof EngineObject3D)
|
|
152
|
+
{
|
|
153
|
+
level3DBaseScale.set(result, result.scale3D.copy());
|
|
154
|
+
result.rotation3D = level3DVector(object.rotation, vec3()).scale(PI / 180);
|
|
155
|
+
result.scale3D = result.scale3D.multiply(level3DVector(object.scale, vec3(1)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// the type's own properties are set on it, one the type has no default for could be a field of the engine's
|
|
159
|
+
for (const key in type.defaults)
|
|
160
|
+
result[key] = properties[key];
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
165
|
+
// the built-in types
|
|
166
|
+
|
|
167
|
+
// a static object of a mesh, with the color, tile and solid properties the built-in types have
|
|
168
|
+
function level3DMakeShape(pos, properties, mesh, tileInfo, asSphere=false)
|
|
169
|
+
{
|
|
170
|
+
const o = new EngineObject3D(pos, mesh, properties.tile >= 0 ? tile(properties.tile) : tileInfo, properties.color);
|
|
171
|
+
o.collideAsSphere3D = asSphere;
|
|
172
|
+
properties.solid && o.setCollision();
|
|
173
|
+
return o;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// the cylinder all Cylinder objects share, built the first time one is made
|
|
177
|
+
let level3DCylinderMesh;
|
|
178
|
+
|
|
179
|
+
level3DAddType('Box', function(pos, properties) { return level3DMakeShape(pos, properties, render3D.boxMesh); },
|
|
180
|
+
{color: WHITE, tile: -1, solid: true});
|
|
181
|
+
level3DAddType('Sphere', function(pos, properties)
|
|
182
|
+
{ return level3DMakeShape(pos, properties, render3D.sphereMesh, undefined, true); },
|
|
183
|
+
{color: WHITE, tile: -1, solid: true});
|
|
184
|
+
level3DAddType('Cylinder', function(pos, properties)
|
|
185
|
+
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
186
|
+
{color: WHITE, tile: -1, solid: true});
|
|
187
|
+
level3DAddType('Light', function(pos, properties)
|
|
188
|
+
{ return new Light3D(pos, properties.radius, properties.color, properties.intensity); },
|
|
189
|
+
{color: WHITE, radius: 5, intensity: 1});
|
|
@@ -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,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?.(); }
|
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;
|