littlejsengine 1.25.0 → 1.26.1
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/CHANGELOG.md +99 -0
- package/FAQ.md +49 -11
- package/README.md +6 -2
- package/REFERENCE.md +129 -21
- package/dist/littlejs.d.ts +487 -102
- package/dist/littlejs.esm.js +2898 -719
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2888 -716
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2702 -527
- package/package.json +4 -4
- package/plugins/box2d.js +110 -34
- package/plugins/gltf.js +74 -22
- package/plugins/lightSystem.js +410 -28
- package/plugins/math3d.js +4 -2
- package/plugins/medalSystem.js +19 -7
- package/plugins/newgrounds.js +4 -2
- package/plugins/parallax.js +1 -0
- package/plugins/particleEffects.js +44 -10
- package/plugins/pathFinder.js +9 -11
- package/plugins/pluginExport.js +9 -0
- package/plugins/postProcess.js +9 -3
- package/plugins/render3d.js +499 -2389
- package/plugins/render3dDebug.js +5 -8
- package/plugins/render3dEditor.js +81 -6
- package/plugins/render3dExtras.js +103 -31
- package/plugins/render3dLevel.js +53 -5
- package/plugins/render3dMesh.js +1158 -0
- package/plugins/render3dObject.js +1451 -0
- package/plugins/render3dVoxels.js +15 -3
- package/plugins/textureSheet.js +9 -8
- package/plugins/threejs.js +1 -1
- package/plugins/tweenSystem.js +76 -51
- package/plugins/uiSystem.js +37 -8
- package/plugins/wavedash.js +229 -0
- package/src/engine.js +199 -40
- package/src/engineAudio.js +37 -21
- package/src/engineBuild.mjs +6 -1
- package/src/engineDebug.js +22 -162
- package/src/engineDraw.js +142 -34
- package/src/engineEditor.js +79 -18
- package/src/engineExport.js +1 -3
- package/src/engineInput.js +34 -24
- package/src/engineMath.js +35 -14
- package/src/engineObject.js +60 -41
- package/src/engineParticles.js +58 -11
- package/src/engineRelease.js +1 -5
- package/src/engineSettings.js +48 -9
- package/src/engineTileLayer.js +11 -4
- package/src/engineUtilities.js +41 -9
- package/src/engineWebGL.js +37 -13
package/plugins/render3dDebug.js
CHANGED
|
@@ -18,15 +18,12 @@ function render3DRenderDebug()
|
|
|
18
18
|
{
|
|
19
19
|
if (!render3DDebugPrimitives.length) return;
|
|
20
20
|
render3DDebugPrimitives = render3DDebugPrimitives.filter(p=> p.clearCount === debugClearCount);
|
|
21
|
-
|
|
21
|
+
render3DWithState({lighting: false, depthTest: false, receiveShadow: false, additive: false, shader: undefined,
|
|
22
|
+
emissiveMap: undefined}, ()=>
|
|
22
23
|
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (const p of render3DDebugPrimitives)
|
|
27
|
-
p.draw();
|
|
28
|
-
});
|
|
29
|
-
}
|
|
24
|
+
for (const p of render3DDebugPrimitives)
|
|
25
|
+
p.draw();
|
|
26
|
+
});
|
|
30
27
|
render3DDebugPrimitives = render3DDebugPrimitives.filter(p=> p.timer < 0); // a Timer compares as negative until it elapses
|
|
31
28
|
}
|
|
32
29
|
|
|
@@ -354,7 +354,8 @@ function editor3DSceneState()
|
|
|
354
354
|
const r = render3D;
|
|
355
355
|
return {sky: r.sky, ambientColor: r.ambientColor.copy(), ambientGroundColor: r.ambientGroundColor?.copy(),
|
|
356
356
|
fogStart: r.fogStart, fogEnd: r.fogEnd, fogColor: r.fogColor?.copy(), sunDirection: r.sunDirection.copy(),
|
|
357
|
-
sunColor: r.sunColor.copy(), shadows: r.shadows, sunFlare: !!level3DSunFlare && !level3DSunFlare.destroyed
|
|
357
|
+
sunColor: r.sunColor.copy(), shadows: r.shadows, sunFlare: !!level3DSunFlare && !level3DSunFlare.destroyed,
|
|
358
|
+
skyBox: r.skyBox, environment: r.environment};
|
|
358
359
|
}
|
|
359
360
|
function editor3DSceneRestore(state)
|
|
360
361
|
{
|
|
@@ -370,6 +371,9 @@ function editor3DSceneRestore(state)
|
|
|
370
371
|
r.sunColor = state.sunColor.copy();
|
|
371
372
|
r.shadows = state.shadows;
|
|
372
373
|
level3DSceneFlare(state.sunFlare); // a flare an earlier level's scene gave the sun, never the game's own
|
|
374
|
+
// the cube maps, and a scene's load still running for them lands no more
|
|
375
|
+
r.skyBox = state.skyBox, r.environment = state.environment;
|
|
376
|
+
++level3DSceneCubeLoads.skyBox, ++level3DSceneCubeLoads.environment;
|
|
373
377
|
}
|
|
374
378
|
|
|
375
379
|
// the scene on screen as a block, to start a level's scene from: a sky only when the dome's colors are known
|
|
@@ -486,6 +490,7 @@ function editor3DHash(text)
|
|
|
486
490
|
// puts in the edits it autosaved for the file, or keeps them waiting when the file changed since
|
|
487
491
|
function editor3DLevelLoaded(level)
|
|
488
492
|
{
|
|
493
|
+
editor3DAutosaveFlush(); // what the last level has waiting is written before this one reads the autosaves
|
|
489
494
|
if (editor3DLevel !== level)
|
|
490
495
|
{
|
|
491
496
|
// another level, the selection and the edit being made were the last one's
|
|
@@ -1147,7 +1152,7 @@ function editor3DStrokeEnd()
|
|
|
1147
1152
|
partsBefore: stroke.parts, partsAfter: parts});
|
|
1148
1153
|
editor3DUndoList.length > 100 && editor3DUndoList.shift();
|
|
1149
1154
|
editor3DRedoList.length = 0;
|
|
1150
|
-
|
|
1155
|
+
editor3DAutosaveSoon();
|
|
1151
1156
|
}
|
|
1152
1157
|
|
|
1153
1158
|
// take the edit being made back, with nothing to undo
|
|
@@ -1171,7 +1176,7 @@ function editor3DUndo(redo=false)
|
|
|
1171
1176
|
(redo ? editor3DUndoList : editor3DRedoList).push(entry);
|
|
1172
1177
|
editor3DSetObjects(redo ? entry.after : entry.before);
|
|
1173
1178
|
editor3DSetParts(redo ? entry.partsAfter : entry.partsBefore);
|
|
1174
|
-
|
|
1179
|
+
editor3DAutosaveSoon();
|
|
1175
1180
|
return true;
|
|
1176
1181
|
}
|
|
1177
1182
|
|
|
@@ -1465,6 +1470,7 @@ function editor3DPrefabClear()
|
|
|
1465
1470
|
// origin, with an undo of its own, and the level it is in put away; false when it is not a prefab's instance
|
|
1466
1471
|
function editor3DPrefabEnter(id)
|
|
1467
1472
|
{
|
|
1473
|
+
editor3DAutosaveFlush(); // the level's waiting autosave, before another level is the one edited
|
|
1468
1474
|
const selected = editor3DSelected();
|
|
1469
1475
|
id ??= selected.length === 1 ? selected[0].id : undefined;
|
|
1470
1476
|
const made = editor3DInstances.get(id), from = editor3DLevel;
|
|
@@ -1647,6 +1653,7 @@ function editor3DSetOpen(open)
|
|
|
1647
1653
|
editor3DMouseOnPanel = false; // the panel hides, with no mouseleave
|
|
1648
1654
|
editor3DStrokeEnd();
|
|
1649
1655
|
while (editor3DPrefabBack()); // the game plays the level, not a prefab that was open
|
|
1656
|
+
editor3DAutosaveFlush(); // the game plays on with what the editor has kept
|
|
1650
1657
|
editor3DToolHeld = false;
|
|
1651
1658
|
editor3DIsOpen = false;
|
|
1652
1659
|
setPaused(editor3DGamePaused);
|
|
@@ -1842,6 +1849,7 @@ function editor3DAutosave(level=editor3DLevel, known)
|
|
|
1842
1849
|
root && editor3DAutosave(Object.keys(prefabs).length ? {...root, prefabs} : root, editor3DRecords.get(root));
|
|
1843
1850
|
return;
|
|
1844
1851
|
}
|
|
1852
|
+
editor3DAutosaveWaiting.delete(level); // written now
|
|
1845
1853
|
const record = known ?? editor3DRecords.get(level);
|
|
1846
1854
|
if (!record || record.pending) return; // edits waiting to be applied keep their autosave
|
|
1847
1855
|
const saves = editor3DSaves(), objects = isArray(level.objects) ? level.objects : [];
|
|
@@ -1853,9 +1861,35 @@ function editor3DAutosave(level=editor3DLevel, known)
|
|
|
1853
1861
|
else
|
|
1854
1862
|
saves[record.key] = {hash: record.hash, savedHash: record.savedHash, objects: editor3DCopy(objects),
|
|
1855
1863
|
...parts};
|
|
1856
|
-
const failed = editorSaveFailed; // the 2D editor's
|
|
1864
|
+
const failed = editorSaveFailed, size = editorAutosaveSize; // the 2D editor's flag and size are its own
|
|
1857
1865
|
editor3DSaveFailed = editorWriteSaves(saves, record.key, editor3DSaveName());
|
|
1858
|
-
|
|
1866
|
+
editor3DAutosaveSize = editorAutosaveSize;
|
|
1867
|
+
editorSaveFailed = failed, editorAutosaveSize = size;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// a level whose autosave writes more than EDITOR_AUTOSAVE_WAIT_SIZE characters, a big terrain's or block map's,
|
|
1871
|
+
// waits until the edits stop for a moment, as the 2D editor's map does, since writing it after every stroke would
|
|
1872
|
+
// cost more than the stroke; a small one is written at once
|
|
1873
|
+
let editor3DAutosaveSize = 0, editor3DAutosaveTimer;
|
|
1874
|
+
const editor3DAutosaveWaiting = new Set;
|
|
1875
|
+
|
|
1876
|
+
// autosave a level after an edit: at once when its autosave is small, or a second after the last of a run of edits;
|
|
1877
|
+
// inside a prefab at once, since its autosave is the level it is in, found through the prefabs open, which Back
|
|
1878
|
+
// changes
|
|
1879
|
+
function editor3DAutosaveSoon(level=editor3DLevel)
|
|
1880
|
+
{
|
|
1881
|
+
if (editor3DAutosaveSize < EDITOR_AUTOSAVE_WAIT_SIZE || editor3DPrefabLevels.has(level))
|
|
1882
|
+
return editor3DAutosave(level);
|
|
1883
|
+
editor3DAutosaveWaiting.add(level);
|
|
1884
|
+
clearTimeout(editor3DAutosaveTimer);
|
|
1885
|
+
editor3DAutosaveTimer = setTimeout(editor3DAutosaveFlush, 1e3);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// write every autosave that is waiting, as the editor closes, the page hides or another level loads
|
|
1889
|
+
function editor3DAutosaveFlush()
|
|
1890
|
+
{
|
|
1891
|
+
clearTimeout(editor3DAutosaveTimer);
|
|
1892
|
+
[...editor3DAutosaveWaiting].forEach((level)=> editor3DAutosave(level));
|
|
1859
1893
|
}
|
|
1860
1894
|
|
|
1861
1895
|
// put the autosaved edits of a file that changed into the level, as one undo
|
|
@@ -2549,11 +2583,16 @@ function editor3DEditorUpdate(seconds)
|
|
|
2549
2583
|
|
|
2550
2584
|
// Ctrl with a letter the editor uses is not the browser's while the editor is open, Ctrl+D would bookmark the page
|
|
2551
2585
|
if (debug && globalThis.document?.addEventListener)
|
|
2586
|
+
{
|
|
2552
2587
|
document.addEventListener('keydown', (e)=>
|
|
2553
2588
|
{
|
|
2554
2589
|
if (editor3DIsOpen && (e.ctrlKey || e.metaKey) && /^Key[ZYCXVDG]$/.test(e.code) && !editorIsTextField(e.target))
|
|
2555
2590
|
e.preventDefault();
|
|
2556
2591
|
});
|
|
2592
|
+
// a page going away writes what is waiting, the last event it is sure to get
|
|
2593
|
+
document.addEventListener('visibilitychange', ()=> document.hidden && editor3DAutosaveFlush());
|
|
2594
|
+
globalThis.addEventListener?.('pagehide', editor3DAutosaveFlush);
|
|
2595
|
+
}
|
|
2557
2596
|
|
|
2558
2597
|
///////////////////////////////////////////////////////////////////////////////
|
|
2559
2598
|
// what the panel does to the level, and what it says
|
|
@@ -3060,7 +3099,8 @@ function editor3DPrefabBoxUpdate(box)
|
|
|
3060
3099
|
unpack.title = 'Ctrl+Shift+G: turn this instance into the objects it is made of';
|
|
3061
3100
|
unpack.onclick = ()=> { editor3DUnpack(); unpack.blur(); };
|
|
3062
3101
|
const save = editorElement('button', line, press, 'Export');
|
|
3063
|
-
save.title = 'Save the prefab as a file of its own, to load with level3DLoadPrefab'
|
|
3102
|
+
save.title = 'Save the prefab as a file of its own, to load with level3DLoadPrefab; a prefab used inside it ' +
|
|
3103
|
+
'is named by its type, and the game adds that one too';
|
|
3064
3104
|
save.onclick = ()=>
|
|
3065
3105
|
{
|
|
3066
3106
|
saveText(editor3DPrefabJSON(instance.prefabName), instance.prefabName + '.json', 'application/json');
|
|
@@ -3398,6 +3438,41 @@ function editor3DSceneUpdate(box)
|
|
|
3398
3438
|
toggle('Shadows', 'The sun casts shadows', !!value('shadows'), (on)=> change('shadows', on));
|
|
3399
3439
|
toggle('Lens flare', 'The sun flares in the lens when it is in view', !!value('lensFlare'), (on)=>
|
|
3400
3440
|
change('lensFlare', on));
|
|
3441
|
+
|
|
3442
|
+
// a sky box of six images, their urls in one field by commas, and whether shiny things reflect it; the
|
|
3443
|
+
// environment follows the sky box while it is the same images
|
|
3444
|
+
const urls = (list)=> isArray(list) && list.length === 6 ? list.join(', ') : '';
|
|
3445
|
+
const skyBox = editorElement('input', line('Sky box', 'Six image urls by commas, +x, -x, +y, -y, +z and -z, a ' +
|
|
3446
|
+
'comma and a space between data urls, loaded as a cube map for the sky; empty for none'), field + ';width:120px');
|
|
3447
|
+
skyBox.value = urls(scene.skyBox);
|
|
3448
|
+
skyBox.onchange = ()=>
|
|
3449
|
+
{
|
|
3450
|
+
// by a comma and a space, so a data url keeps its own comma, or by commas alone when that is not six
|
|
3451
|
+
const value = skyBox.value.replace(/[,\s]+$/, ''); // a comma left at the end is not part of the last url
|
|
3452
|
+
const split = (by)=> value.split(by).map((url)=> url.trim()).filter((url)=> url);
|
|
3453
|
+
let list = split(/,\s+/);
|
|
3454
|
+
list.length === 6 || (list = split(','));
|
|
3455
|
+
if (list.length && list.length !== 6)
|
|
3456
|
+
skyBox.value = urls(editor3DScene()?.skyBox); // not six, what it has now shows again
|
|
3457
|
+
else editor3DChangeScene((s={})=>
|
|
3458
|
+
{
|
|
3459
|
+
const {skyBox, environment, ...rest} = s, reflected = !!urls(skyBox) && urls(environment) === urls(skyBox);
|
|
3460
|
+
return {...rest, ...list.length && {skyBox: list},
|
|
3461
|
+
...(reflected ? list.length && {environment: [...list]} : environment && {environment})};
|
|
3462
|
+
});
|
|
3463
|
+
editor3DStrokeEnd();
|
|
3464
|
+
skyBox.blur();
|
|
3465
|
+
};
|
|
3466
|
+
if (urls(scene.skyBox))
|
|
3467
|
+
toggle('Reflect it', 'Shiny things reflect the sky box, as render3D.environment',
|
|
3468
|
+
urls(scene.environment) === urls(scene.skyBox), (on)=> editor3DChangeScene((s={})=>
|
|
3469
|
+
{
|
|
3470
|
+
const {environment, ...rest} = s;
|
|
3471
|
+
return on && isArray(s.skyBox) ? {...rest, environment: [...s.skyBox]} : rest;
|
|
3472
|
+
}));
|
|
3473
|
+
else if (urls(scene.environment)) // one of its own, with no sky box, can still be taken out
|
|
3474
|
+
toggle('Environment', 'The level\'s own environment, six images that shiny things reflect; off takes it out',
|
|
3475
|
+
true, (on)=> on || editor3DChangeScene((s={})=> { const {environment, ...rest} = s; return rest; }));
|
|
3401
3476
|
}
|
|
3402
3477
|
|
|
3403
3478
|
// the properties box: the position, rotation and scale of the one selected object and an input for each default
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
// the way the 2D font does, but extruded glyphs seen from an angle then overlap the line below
|
|
15
15
|
const RENDER3D_TEXT_LEADING = 1.3;
|
|
16
16
|
|
|
17
|
-
// let go of the parent but stay where the object was in the world, which removeChild keeps by itself; a
|
|
18
|
-
// parent has already let go, so the
|
|
17
|
+
// let go of the parent but stay where and as the object was in the world, which removeChild keeps by itself; a
|
|
18
|
+
// destroyed parent has already let go, so the world matrix last built with it in stands in, scale and turn as well
|
|
19
19
|
function render3DDetach(o)
|
|
20
20
|
{
|
|
21
21
|
if (o.parent)
|
|
22
22
|
o.parent.removeChild(o);
|
|
23
|
-
else if (o.
|
|
24
|
-
o
|
|
23
|
+
else if (o.matrixParent)
|
|
24
|
+
render3DTakeWorld(o, o.worldMatrix);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// a soft white dot for untextured particles, made once from a canvas, undefined headless or without a canvas
|
|
@@ -404,13 +404,14 @@ function buildText3D(text, size=1, depth=.2, font=engineImageFont)
|
|
|
404
404
|
let glyphs = render3DGlyphCache.get(font); // unit sized, scaled when combined
|
|
405
405
|
glyphs || render3DGlyphCache.set(font, glyphs = new Map);
|
|
406
406
|
const charSize = vec2(size * tileInfo.size.x / tileInfo.size.y, size);
|
|
407
|
-
const mesh = new Mesh, lines = (text
|
|
407
|
+
const mesh = new Mesh, lines = textLines(text); // a Windows line ending too
|
|
408
408
|
lines.forEach((line, j)=>
|
|
409
409
|
{
|
|
410
410
|
const y = ((lines.length - 1) / 2 - j) * charSize.y * RENDER3D_TEXT_LEADING;
|
|
411
|
-
|
|
411
|
+
const characters = textCharacters(line); // as ImageFont reads them, an emoji one box
|
|
412
|
+
for (let i = 0; i < characters.length; ++i)
|
|
412
413
|
{
|
|
413
|
-
const charCode =
|
|
414
|
+
const charCode = textCharacterCode(characters, i);
|
|
414
415
|
const index = charCode < 32 || charCode > 127 ? 95 : charCode - 32; // like ImageFont
|
|
415
416
|
if (!index) continue; // space
|
|
416
417
|
let glyph = glyphs.get(index);
|
|
@@ -419,7 +420,7 @@ function buildText3D(text, size=1, depth=.2, font=engineImageFont)
|
|
|
419
420
|
const pos = font.getGlyphPos(index); // where ImageFont finds it
|
|
420
421
|
glyphs.set(index, glyph = buildExtrude(new TileInfo(pos, tileInfo.size, tileInfo.textureInfo)));
|
|
421
422
|
}
|
|
422
|
-
const x = (i - (
|
|
423
|
+
const x = (i - (characters.length - 1) / 2) * charSize.x;
|
|
423
424
|
mesh.combine(glyph, buildMatrix(vec3(x, y, 0), undefined, vec3(charSize.x, charSize.y, depth)));
|
|
424
425
|
}
|
|
425
426
|
});
|
|
@@ -630,11 +631,15 @@ class HeightMap extends EngineObject3D
|
|
|
630
631
|
* @ignore */
|
|
631
632
|
levelSegment3D(from, to)
|
|
632
633
|
{
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
634
|
+
// a move above the highest the ground goes misses it, which is most particles most of the time
|
|
635
|
+
const top = this.pos3D.y + max(this.height, 0);
|
|
636
|
+
if (from.y > top && to.y > top) return undefined;
|
|
637
|
+
if (from.y < this.getHeight(from.x, from.z)) return undefined; // under from the start, let go
|
|
638
|
+
// the ground is flat in each triangle, not between the ends, so a ridge both ends are above is still met:
|
|
639
|
+
// the exact raycast along the move, which walks every triangle it crosses, the first one it goes under
|
|
640
|
+
const distance = this.raycast(new Ray3D(from, to.subtract(from)));
|
|
641
|
+
// over the move, and not a touch where it starts on the surface and leaves it
|
|
642
|
+
if (distance === undefined || distance > 1 || !distance && to.y >= this.getHeight(to.x, to.z)) return undefined;
|
|
638
643
|
return {distance, normal: this.getNormal(from.x + (to.x - from.x) * distance, from.z + (to.z - from.z) * distance)};
|
|
639
644
|
}
|
|
640
645
|
|
|
@@ -647,11 +652,26 @@ class HeightMap extends EngineObject3D
|
|
|
647
652
|
{
|
|
648
653
|
const p = o.pos3D, m = this.pos3D, size = this.mapSize;
|
|
649
654
|
if (abs(p.x - m.x) > size.x / 2 || abs(p.z - m.z) > size.y / 2) return; // off the map
|
|
650
|
-
const half = o.size3D.y * abs(o.scale3D.y) / 2
|
|
651
|
-
|
|
652
|
-
//
|
|
653
|
-
|
|
654
|
-
|
|
655
|
+
const half = o.size3D.y * abs(o.scale3D.y) / 2;
|
|
656
|
+
let ground = this.getHeight(p.x, p.z);
|
|
657
|
+
// only the ends of a move are kept above the ground, so a ridge it goes over is met by a sweep, as a
|
|
658
|
+
// particle's move is, a quarter of its height up from its foot, from where it was to where it would stand: a
|
|
659
|
+
// bump lower than that is walked over as before, and it goes up on the near side of anything taller
|
|
660
|
+
const lift = half / 2, endFoot = max(p.y - half, ground) + lift;
|
|
661
|
+
const hit = oldPos && this.levelSegment3D(vec3(oldPos.x, oldPos.y - half + lift, oldPos.z), vec3(p.x, endFoot, p.z));
|
|
662
|
+
if (hit && hit.distance)
|
|
663
|
+
{
|
|
664
|
+
p.x = oldPos.x + (p.x - oldPos.x) * hit.distance;
|
|
665
|
+
p.z = oldPos.z + (p.z - oldPos.z) * hit.distance;
|
|
666
|
+
ground = this.getHeight(p.x, p.z);
|
|
667
|
+
}
|
|
668
|
+
else
|
|
669
|
+
{
|
|
670
|
+
// one that stood on it and is not rising keeps to it going downhill, as far down as it moved across, so
|
|
671
|
+
// it stays grounded down a slope as steep as 45 degrees instead of falling in small hops
|
|
672
|
+
const follow = wasOn && o.velocity3D.y <= 0 ? hypot(p.x - oldPos.x, p.z - oldPos.z) : 0;
|
|
673
|
+
if (p.y - half > ground + follow) return;
|
|
674
|
+
}
|
|
655
675
|
p.y = ground + half;
|
|
656
676
|
const v = o.velocity3D;
|
|
657
677
|
if (v.y < 0)
|
|
@@ -1003,7 +1023,8 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1003
1023
|
/** @property {boolean} - Flatten the spawn area across the way it emits, its own up: a sphere becomes a disc
|
|
1004
1024
|
* and a box a flat rectangle, for rain from a sheet of sky or flames from a patch of ground */
|
|
1005
1025
|
this.emitFlat = false;
|
|
1006
|
-
/** @property {number} - How long to keep emitting, 0 is forever
|
|
1026
|
+
/** @property {number} - How long to keep emitting, 0 is forever; raised while its particles are still alive,
|
|
1027
|
+
* it emits again for the added time */
|
|
1007
1028
|
this.emitTime = emitTime;
|
|
1008
1029
|
/** @property {number} - Particles per second, 0 does not emit */
|
|
1009
1030
|
this.emitRate = emitRate;
|
|
@@ -1081,6 +1102,12 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1081
1102
|
* @type {Vector3|undefined} */
|
|
1082
1103
|
this.worldPos3D = undefined;
|
|
1083
1104
|
this.emitTimeBuffer = 1; // the first particle comes at once, as the 2D emitter's does
|
|
1105
|
+
this.emitElapsed = 0; // seconds of its emit time it has emitted for, counted by its updates
|
|
1106
|
+
// where it was last update, and how far back toward it the particle being made starts, 0 to 1: a moving
|
|
1107
|
+
// emitter spreads an update's particles along its move, as the 2D emitter does
|
|
1108
|
+
/** @type {Vector3|undefined} */
|
|
1109
|
+
this.emitFrom = undefined;
|
|
1110
|
+
this.emitBehind = 0;
|
|
1084
1111
|
}
|
|
1085
1112
|
|
|
1086
1113
|
/** Spawn new particles, move the live ones, and go away when done */
|
|
@@ -1088,21 +1115,37 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1088
1115
|
{
|
|
1089
1116
|
// one transform for the frame: where the emitter is, and how big the effect it makes is
|
|
1090
1117
|
const matrix = render3DObjectMatrix(this); // the object's own, read only
|
|
1118
|
+
this.emitFrom = this.worldPos3D;
|
|
1091
1119
|
this.worldPos3D = matrix.getTranslation(); // remembered for when the parent is destroyed
|
|
1092
1120
|
const scale = render3DMaxScale(matrix.m);
|
|
1093
1121
|
this.particleView.scale = scale; // the callbacks read it from the view instead of working it out each time
|
|
1094
1122
|
|
|
1095
1123
|
// emit until the emit time is up, then wait for the last particle and go away
|
|
1096
|
-
if (!this.emitTime || this.
|
|
1124
|
+
if (!this.emitTime || this.emitElapsed < this.emitTime)
|
|
1097
1125
|
{
|
|
1098
1126
|
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
1099
|
-
// neither is a reason to stop counting down the emit time
|
|
1127
|
+
// neither is a reason to stop counting down the emit time; the part of this update inside the emit time,
|
|
1128
|
+
// counted by its own updates, as the 2D emitter does, so a one shot gives the same count on any frame
|
|
1100
1129
|
const rate = this.emitRate * particleEmitRateScale;
|
|
1130
|
+
const step = this.emitTime ? min(timeDelta, this.emitTime - this.emitElapsed) : timeDelta;
|
|
1131
|
+
this.emitElapsed += timeDelta;
|
|
1101
1132
|
if (rate > 0 && rate < Infinity)
|
|
1102
1133
|
{
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1134
|
+
// the update that ends the emit time adds a hair, as the steps' sum comes out a rounding under it
|
|
1135
|
+
this.emitTimeBuffer += rate * step + (this.emitTime && this.emitElapsed >= this.emitTime ? 1e-6 : 0);
|
|
1136
|
+
// spread along the move since the last update, the last one where it is now; a jump further than
|
|
1137
|
+
// anything travels in an update, twice objectMaxSpeed, a teleport or a respawn, spreads nothing
|
|
1138
|
+
const travelled = this.emitFrom && this.worldPos3D.distance(this.emitFrom) <= 2 * objectMaxSpeed;
|
|
1139
|
+
const count = floor(this.emitTimeBuffer);
|
|
1140
|
+
try
|
|
1141
|
+
{
|
|
1142
|
+
for (let i = count; i--; --this.emitTimeBuffer)
|
|
1143
|
+
{
|
|
1144
|
+
this.emitBehind = travelled ? i / count : 0;
|
|
1145
|
+
this.emitParticle();
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
finally { this.emitBehind = 0; } // a create callback that throws leaves no offset for the next
|
|
1106
1149
|
}
|
|
1107
1150
|
}
|
|
1108
1151
|
else if (!this.particleCount)
|
|
@@ -1129,7 +1172,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1129
1172
|
const damping = this.damping, angleDamping = this.angleDamping, g = render3D.gravity, share = this.gravityScale;
|
|
1130
1173
|
const gravityX = g.x * share * scale, gravityY = (this.gravity + g.y * share) * scale;
|
|
1131
1174
|
const gravityZ = g.z * share * scale;
|
|
1132
|
-
const collideLevel = this.collideLevel && render3DLevel.length;
|
|
1175
|
+
const collideLevel = this.collideLevel && enablePhysicsSolver && render3DLevel.length; // as 2D particles
|
|
1133
1176
|
const updateCallback = this.particleUpdateCallback, destroyCallback = this.particleDestroyCallback;
|
|
1134
1177
|
for (let i = this.particleCount; i--;)
|
|
1135
1178
|
{
|
|
@@ -1204,6 +1247,12 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1204
1247
|
const direction = matrix.transformDirection(randVector3(1, this.emitConeAngle)).normalize();
|
|
1205
1248
|
|
|
1206
1249
|
const pos = matrix.transformPoint(offset), speed = this.speed * random() * scale;
|
|
1250
|
+
const from = this.emitFrom, behind = this.emitBehind; // back along its move, as the update spreads them
|
|
1251
|
+
if (from && behind)
|
|
1252
|
+
{
|
|
1253
|
+
const here = this.worldPos3D;
|
|
1254
|
+
pos.x += (from.x - here.x) * behind, pos.y += (from.y - here.y) * behind, pos.z += (from.z - here.z) * behind;
|
|
1255
|
+
}
|
|
1207
1256
|
const colorStart = randColor(this.colorStartA, this.colorStartB, true), colorEnd = randColor(this.colorEndA, this.colorEndB, true);
|
|
1208
1257
|
|
|
1209
1258
|
// room for one more, doubling as the set grows, the trails along with it
|
|
@@ -1233,11 +1282,13 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1233
1282
|
data[k+18] = this.angleSpeed ? rand(2*PI) : 0;
|
|
1234
1283
|
data[k+19] = this.angleSpeed ? this.angleSpeed * random() * randSign() : 0;
|
|
1235
1284
|
data[k+20] = 0; // trail points
|
|
1285
|
+
this.particleView.scale = scale; // the scale it was made at, emitted before the first update too
|
|
1236
1286
|
this.particleCreateCallback && this.particleCall(this.particleCreateCallback, k);
|
|
1237
1287
|
}
|
|
1238
1288
|
|
|
1239
1289
|
// call a particle callback with the particle at k: the emitter's one Particle3D is set from its numbers, and
|
|
1240
1290
|
// what the callback changes is written back; a particle it destroys has lived its life, the update removes it
|
|
1291
|
+
/** @private */
|
|
1241
1292
|
particleCall(callback, k, level, pos)
|
|
1242
1293
|
{
|
|
1243
1294
|
// a callback that emits runs the create callback inside itself, which gets a view of its own so this one
|
|
@@ -1245,6 +1296,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1245
1296
|
// emit into a full emitter grows them
|
|
1246
1297
|
const nested = render3DParticlesCalling.has(this);
|
|
1247
1298
|
const view = nested ? render3DParticleView(this) : this.particleView, data = this.particleData;
|
|
1299
|
+
view.scale = this.particleView.scale; // a nested view's too
|
|
1248
1300
|
view.pos.set(data[k], data[k+1], data[k+2]);
|
|
1249
1301
|
view.velocity.set(data[k+3], data[k+4], data[k+5]);
|
|
1250
1302
|
view.age = data[k+17], view.lifeTime = data[k+16], view.destroyed = false;
|
|
@@ -1264,6 +1316,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
1264
1316
|
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
1265
1317
|
// collide callback lets it through; one that starts inside is let go, as a 2D one is; true when the callback
|
|
1266
1318
|
// destroyed it
|
|
1319
|
+
/** @private */
|
|
1267
1320
|
particleCollide(k, x, y, z)
|
|
1268
1321
|
{
|
|
1269
1322
|
let data = this.particleData;
|
|
@@ -1634,8 +1687,9 @@ class LensFlare3D extends EngineObject3D
|
|
|
1634
1687
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
1635
1688
|
this.fadeTime = .15;
|
|
1636
1689
|
/** @property {number} - How much of the sun shows, 0 hidden, off the screen or behind the camera to 1 in
|
|
1637
|
-
* plain view, eased */
|
|
1638
|
-
this.visible =
|
|
1690
|
+
* plain view, eased; the first update sets it at once, so a sun hidden from the start never shows */
|
|
1691
|
+
this.visible = 0;
|
|
1692
|
+
this.visibleFound = false; // the first update has found it
|
|
1639
1693
|
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
1640
1694
|
this.madeKey = '';
|
|
1641
1695
|
/** @type {Array<LensFlareElement>} */
|
|
@@ -1736,6 +1790,13 @@ class LensFlare3D extends EngineObject3D
|
|
|
1736
1790
|
* tileInfo: TileInfo|undefined, angle: number}>} */
|
|
1737
1791
|
getScreenElements()
|
|
1738
1792
|
{
|
|
1793
|
+
// one made while the game is paused, as the 3D editor makes them, has had no update to find it, and while
|
|
1794
|
+
// paused nothing updates it, so it is found at once each time it is drawn, as the camera turns
|
|
1795
|
+
if (!this.visibleFound || paused)
|
|
1796
|
+
{
|
|
1797
|
+
this.visibleFound = false;
|
|
1798
|
+
this.updateVisible(0);
|
|
1799
|
+
}
|
|
1739
1800
|
const look = this.visible > 0 && this.flareLook();
|
|
1740
1801
|
if (!look) return [];
|
|
1741
1802
|
const {sun, center, tint, height} = look, strength = look.strength * this.visible;
|
|
@@ -1759,7 +1820,7 @@ class LensFlare3D extends EngineObject3D
|
|
|
1759
1820
|
if (!source) return true;
|
|
1760
1821
|
const ray = new Ray3D(render3D.camera.pos, source.direction), reach = source.distance;
|
|
1761
1822
|
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.destroyed &&
|
|
1762
|
-
!o.transparent && !o
|
|
1823
|
+
!o.transparent && !render3DSetting(o, 'additive'));
|
|
1763
1824
|
// a voxel map says block by block what is see through: glass, water and leaves let the sun by, and the
|
|
1764
1825
|
// ray goes on to the blocks behind them
|
|
1765
1826
|
const maps = /** @type {Array<VoxelMap>} */ (typeof VoxelMap == 'undefined' ? [] :
|
|
@@ -1800,9 +1861,20 @@ class LensFlare3D extends EngineObject3D
|
|
|
1800
1861
|
super.update();
|
|
1801
1862
|
if (this.light?.destroyed)
|
|
1802
1863
|
return this.destroy(); // the flare of a light that is gone
|
|
1864
|
+
this.updateVisible(timeDelta);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/** Ease visible toward whether the sun shows over some seconds; the first time it is found at once, from the
|
|
1868
|
+
* camera as it is, its matrices made, by the first update or the first draw, whichever comes first
|
|
1869
|
+
* @param {number} seconds
|
|
1870
|
+
* @ignore */
|
|
1871
|
+
updateVisible(seconds)
|
|
1872
|
+
{
|
|
1873
|
+
this.visibleFound || render3D.updateMatrices();
|
|
1803
1874
|
const shows = !!this.flareLook() && !(this.occlusion && this.isHidden());
|
|
1804
|
-
const step = this.fadeTime > 0 ?
|
|
1875
|
+
const step = this.fadeTime > 0 && this.visibleFound ? seconds / this.fadeTime : 1;
|
|
1805
1876
|
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
1877
|
+
this.visibleFound = true;
|
|
1806
1878
|
}
|
|
1807
1879
|
|
|
1808
1880
|
/** Draw the flare over the 3D scene, added onto it, called automatically in the 2D pass */
|
|
@@ -1918,9 +1990,9 @@ function parseOBJ(text, smooth=render3D?.smoothShading)
|
|
|
1918
1990
|
*/
|
|
1919
1991
|
async function loadOBJ(url, smooth=render3D?.smoothShading)
|
|
1920
1992
|
{
|
|
1921
|
-
const response = await
|
|
1993
|
+
const response = await loadFetch(url, 'loadOBJ');
|
|
1922
1994
|
if (!response.ok)
|
|
1923
|
-
throw new Error(
|
|
1995
|
+
throw new Error(`loadOBJ: could not load ${url}, ${response.status} ${response.statusText}`);
|
|
1924
1996
|
const text = await response.text(), mesh = parseOBJ(text, smooth);
|
|
1925
1997
|
// a mesh of nothing draws nothing, so it says why, a web page being what a dev server sends for a mistyped path
|
|
1926
1998
|
mesh.points.length || console.warn('loadOBJ: ' + url + ' has no faces' +
|
package/plugins/render3dLevel.js
CHANGED
|
@@ -19,7 +19,8 @@ const LEVEL3D_VERSION = 1; // the format a level's littlejs3D names
|
|
|
19
19
|
* @typedef {Object} Level3D
|
|
20
20
|
* @property {number} [littlejs3D] - The format's version, 1
|
|
21
21
|
* @property {Array<Object>} objects - Each {id, type, pos, rotation, scale, properties}, all but type optional
|
|
22
|
-
* @property {Object} [scene] - The scene block: sky, ambient, sunDirection, sunColor, fog, fogColor, shadows, lensFlare
|
|
22
|
+
* @property {Object} [scene] - The scene block: sky, ambient, sunDirection, sunColor, fog, fogColor, shadows, lensFlare,
|
|
23
|
+
* skyBox, environment
|
|
23
24
|
* @property {Object} [voxels] - A block map: {pos, size, blocks}
|
|
24
25
|
* @property {Object} [terrain] - A height map: {pos, size, height, color, heights, paint}
|
|
25
26
|
* @property {Object} [prefabs] - The level's own prefabs by name, each {objects, attached}
|
|
@@ -94,9 +95,10 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
94
95
|
* are added before its objects are made, one the game added itself keeps its place
|
|
95
96
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
96
97
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
97
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows,
|
|
98
|
-
* sun's lens flare
|
|
99
|
-
*
|
|
98
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, lensFlare, the
|
|
99
|
+
* sun's lens flare, and skyBox and environment, each six image urls as loadCubeMap takes them, which load in
|
|
100
|
+
* the background and are set when they have; what the block leaves out stays as the game set it, and a level
|
|
101
|
+
* with no block changes nothing
|
|
100
102
|
* @param {Level3D} level - The level, the level editor edits this same object
|
|
101
103
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
102
104
|
* @memberof Level3D */
|
|
@@ -300,6 +302,8 @@ const level3DSkies = new WeakSet;
|
|
|
300
302
|
// and shadows; a setting the block does not have, or has wrong, stays as it is
|
|
301
303
|
function level3DSceneApply(scene)
|
|
302
304
|
{
|
|
305
|
+
// a sky box or environment an earlier scene is still loading does not land on this one's
|
|
306
|
+
++level3DSceneCubeLoads.skyBox, ++level3DSceneCubeLoads.environment;
|
|
303
307
|
const r = render3D;
|
|
304
308
|
if (!r || !scene || typeof scene !== 'object') return;
|
|
305
309
|
const color = level3DHexColor;
|
|
@@ -326,6 +330,47 @@ function level3DSceneApply(scene)
|
|
|
326
330
|
r.shadows = scene.shadows;
|
|
327
331
|
if (typeof scene.lensFlare === 'boolean')
|
|
328
332
|
level3DSceneFlare(scene.lensFlare);
|
|
333
|
+
level3DSceneCubeMap('skyBox', scene.skyBox);
|
|
334
|
+
level3DSceneCubeMap('environment', scene.environment);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// the cube maps scene blocks loaded, by their six urls, the four used last, so the same ones load once; a load that
|
|
338
|
+
// failed is not kept, and is tried again by the next scene that names it
|
|
339
|
+
const level3DSceneCubeMaps = new Map;
|
|
340
|
+
const level3DSceneCubeLoaded = new WeakMap; // the cube map each of their loads gave, once it has
|
|
341
|
+
|
|
342
|
+
// which load each of render3D.skyBox and environment waits for: every scene applied, or the editor putting the
|
|
343
|
+
// game's back, moves it on, and a load that finishes after is dropped
|
|
344
|
+
const level3DSceneCubeLoads = {skyBox: 0, environment: 0};
|
|
345
|
+
|
|
346
|
+
// load the six urls a scene block gives for render3D.skyBox or environment and set it when they have loaded; a value
|
|
347
|
+
// that is not six strings leaves it as it is, and a failed load says so and leaves it too
|
|
348
|
+
function level3DSceneCubeMap(field, urls)
|
|
349
|
+
{
|
|
350
|
+
if (!isArray(urls) || urls.length !== 6 || !urls.every((url)=> typeof url === 'string')) return;
|
|
351
|
+
const key = urls.join('\n'), load = ++level3DSceneCubeLoads[field], before = render3D[field];
|
|
352
|
+
// kept by the urls, newest last: one used again moves to the end, and past four the oldest goes, its texture
|
|
353
|
+
// freed unless it is still on screen
|
|
354
|
+
let cube = level3DSceneCubeMaps.get(key);
|
|
355
|
+
level3DSceneCubeMaps.delete(key);
|
|
356
|
+
level3DSceneCubeMaps.set(key, cube ||= loadCubeMap(urls));
|
|
357
|
+
cube.then((map)=> level3DSceneCubeLoaded.set(cube, map), ()=> {});
|
|
358
|
+
for (const [oldKey, old] of level3DSceneCubeMaps)
|
|
359
|
+
{
|
|
360
|
+
if (level3DSceneCubeMaps.size <= 4) break;
|
|
361
|
+
// one on screen stays, so it is not loaded again and the copy on screen left behind
|
|
362
|
+
const map = level3DSceneCubeLoaded.get(old);
|
|
363
|
+
if (map && (map === render3D.skyBox || map === render3D.environment)) continue;
|
|
364
|
+
level3DSceneCubeMaps.delete(oldKey);
|
|
365
|
+
old.then((map)=> map !== render3D.skyBox && map !== render3D.environment && map.dispose(), ()=> {});
|
|
366
|
+
}
|
|
367
|
+
// set when it has loaded, unless a newer scene came, or the game set the field itself meanwhile
|
|
368
|
+
const landed = (map)=> load === level3DSceneCubeLoads[field] && render3D[field] === before && (render3D[field] = map);
|
|
369
|
+
cube.then(landed, (error)=>
|
|
370
|
+
{
|
|
371
|
+
level3DSceneCubeMaps.get(key) === cube && level3DSceneCubeMaps.delete(key);
|
|
372
|
+
console.warn(error.message);
|
|
373
|
+
});
|
|
329
374
|
}
|
|
330
375
|
|
|
331
376
|
// the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
|
|
@@ -544,12 +589,14 @@ class Prefab3D extends EngineObject3D
|
|
|
544
589
|
this.prefabName = prefabName;
|
|
545
590
|
/** @property {boolean} - Are its parts its children, moving with it as one body */
|
|
546
591
|
this.attached = !!level3DPrefabs.get(prefabName)?.attached || level3DPrefabAttached > 0;
|
|
547
|
-
/** @property {Array<
|
|
592
|
+
/** @property {Array<EngineObject3D>} - What the prefab's objects made, in the prefab's order
|
|
593
|
+
* @type {Array<EngineObject3D>} */
|
|
548
594
|
this.parts = [];
|
|
549
595
|
/** @property {Vector3} - From the prefab's origin to the handle, in the prefab's own space: nothing for
|
|
550
596
|
* separate parts, and for an attached prefab the middle of the box around its parts, where its handle
|
|
551
597
|
* is, as an object's place is the middle of its body */
|
|
552
598
|
this.originOffset = vec3();
|
|
599
|
+
/** @type {Array<{id?: number, type: string, pos?: Array<number>, rotation?: Array<number>, scale?: Array<number>|number, properties?: Object}>} */
|
|
553
600
|
this.partObjects = []; // the prefab's object of each part
|
|
554
601
|
this.partsPlaced = ''; // where the handle was when its parts were last placed, undefined parts not made
|
|
555
602
|
this.partsMade = false;
|
|
@@ -641,6 +688,7 @@ class Prefab3D extends EngineObject3D
|
|
|
641
688
|
for (const part of children)
|
|
642
689
|
{
|
|
643
690
|
part.pos3D = part.pos3D.subtract(this.originOffset);
|
|
691
|
+
render3DShareSettings(part, this); // drawn with what is set on the handle, its own otherwise
|
|
644
692
|
this.addChild(part);
|
|
645
693
|
}
|
|
646
694
|
}
|