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
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.
|
|
38
|
+
const engineVersion = '1.23.0';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -982,8 +982,19 @@ function tweak(){}
|
|
|
982
982
|
function tweakButton(){}
|
|
983
983
|
function tweakDivider(){}
|
|
984
984
|
function tweakEngineDefaults(){}
|
|
985
|
-
|
|
986
|
-
|
|
985
|
+
class LevelEditor
|
|
986
|
+
{
|
|
987
|
+
constructor() { this.paletteTiles = this.use3D = this.tool = undefined; }
|
|
988
|
+
get isOpen() { return false; }
|
|
989
|
+
get is3D() { return false; }
|
|
990
|
+
get edit2D() { return undefined; }
|
|
991
|
+
get edit3D() { return undefined; }
|
|
992
|
+
open(){} close(){} addKey(){} addButton(){} addTool(){}
|
|
993
|
+
onTile(){} onRestart(){} onPlayFrom(){} onOpen(){} onClose(){} onUpdate(){} onDraw(){} onPanel(){}
|
|
994
|
+
onSave(){ return false; }
|
|
995
|
+
}
|
|
996
|
+
let levelEditor = new LevelEditor;
|
|
997
|
+
function setLevelEditor(editor){ levelEditor = editor; }
|
|
987
998
|
function editorMapRestore(map){ return map; }
|
|
988
999
|
function editorMapLoaded(){}
|
|
989
1000
|
function editorJSONFetched(){}
|
|
@@ -2884,6 +2895,15 @@ let touchGamepadEnable = false;
|
|
|
2884
2895
|
* @memberof Settings */
|
|
2885
2896
|
let touchGamepadPassthrough = false;
|
|
2886
2897
|
|
|
2898
|
+
/** True if a pinch of two fingers turns the mouse wheel: touchPinch is added to mouseWheel, so what zooms with
|
|
2899
|
+
* the wheel zooms with a pinch, the mouse is between the two fingers, and a second finger lets go of the button
|
|
2900
|
+
* the first pressed, since a pinch is not a drag; turn it off for a game with its own use for two fingers, the
|
|
2901
|
+
* first finger stays the mouse and touchPinch can still be read
|
|
2902
|
+
* @type {boolean}
|
|
2903
|
+
* @default
|
|
2904
|
+
* @memberof Settings */
|
|
2905
|
+
let touchPinchWheel = true;
|
|
2906
|
+
|
|
2887
2907
|
/** Size of center button if touch gamepad should have start button in the center
|
|
2888
2908
|
* - Prevents activating when pressed near virtual stick or face buttons
|
|
2889
2909
|
* - When the game is paused, any touch will press the button
|
|
@@ -3279,6 +3299,11 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
|
|
|
3279
3299
|
* @memberof Settings */
|
|
3280
3300
|
function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
3281
3301
|
|
|
3302
|
+
/** Set if a pinch of two fingers turns the mouse wheel
|
|
3303
|
+
* @param {boolean} enable
|
|
3304
|
+
* @memberof Settings */
|
|
3305
|
+
function setTouchPinchWheel(enable) { touchPinchWheel = enable; }
|
|
3306
|
+
|
|
3282
3307
|
/** Set if touches outside the gamepad controls should still drive mouse/touch input
|
|
3283
3308
|
* @param {boolean} passthrough
|
|
3284
3309
|
* @memberof Settings */
|
|
@@ -4585,17 +4610,29 @@ class SpriteAnimator
|
|
|
4585
4610
|
* @return {SpriteAnimation} */
|
|
4586
4611
|
get clip() { return this.clips[this.name]; }
|
|
4587
4612
|
|
|
4613
|
+
/** The clip showing now, once a clip that has ended has called its onEnd: the callback may switch clips, and a
|
|
4614
|
+
* read gives the clip it switched to, not the one that ended
|
|
4615
|
+
* @return {SpriteAnimation}
|
|
4616
|
+
* @ignore */
|
|
4617
|
+
get settledClip()
|
|
4618
|
+
{
|
|
4619
|
+
// each clip's own isDone calls its onEnd once; a few switches in a row at most
|
|
4620
|
+
for (let i = 4, clip; i-- && clip !== this.clip;)
|
|
4621
|
+
(clip = this.clip).isDone;
|
|
4622
|
+
return this.clip;
|
|
4623
|
+
}
|
|
4624
|
+
|
|
4588
4625
|
/** The tile of the frame showing now
|
|
4589
4626
|
* @return {TileInfo} */
|
|
4590
|
-
get tileInfo() { return this.
|
|
4627
|
+
get tileInfo() { return this.settledClip.tileInfo; }
|
|
4591
4628
|
|
|
4592
4629
|
/** The frame of the clip showing now
|
|
4593
4630
|
* @return {number} */
|
|
4594
|
-
get frame() { return this.
|
|
4631
|
+
get frame() { return this.settledClip.frame; }
|
|
4595
4632
|
|
|
4596
4633
|
/** True once the clip showing now is a play that has ended
|
|
4597
4634
|
* @return {boolean} */
|
|
4598
|
-
get isDone() { return this.
|
|
4635
|
+
get isDone() { return this.settledClip.isDone; }
|
|
4599
4636
|
}
|
|
4600
4637
|
|
|
4601
4638
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6034,6 +6071,13 @@ let mouseDeltaScreen = vec2();
|
|
|
6034
6071
|
* @memberof Input */
|
|
6035
6072
|
let mouseWheel = 0;
|
|
6036
6073
|
|
|
6074
|
+
/** Touch pinch delta this frame, in the mouse wheel's units: two fingers moving apart are negative, as the wheel
|
|
6075
|
+
* turned up is, and moving together positive, 1 for every 50 pixels; added to mouseWheel while touchPinchWheel
|
|
6076
|
+
* is on, so a game that zooms with the wheel zooms with a pinch
|
|
6077
|
+
* @type {number}
|
|
6078
|
+
* @memberof Input */
|
|
6079
|
+
let touchPinch = 0;
|
|
6080
|
+
|
|
6037
6081
|
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
6038
6082
|
* @type {boolean}
|
|
6039
6083
|
* @memberof Input */
|
|
@@ -6122,6 +6166,7 @@ function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearRe
|
|
|
6122
6166
|
const inputWASDToArrow = {KeyW:'ArrowUp', KeyS:'ArrowDown', KeyA:'ArrowLeft', KeyD:'ArrowRight'};
|
|
6123
6167
|
const inputArrowToWASD = {ArrowUp:'KeyW', ArrowDown:'KeyS', ArrowLeft:'KeyA', ArrowRight:'KeyD'};
|
|
6124
6168
|
const inputKeysHeld = new Set; // the keys physically down, since an arrow's slot is shared with its alias
|
|
6169
|
+
let inputPinch; // the two fingers pinching and how far apart they were, undefined with no pinch
|
|
6125
6170
|
let inputWasTouching = 0, inputTouchIdentifier; // the touch driving the mouse, cleared with the input so only a new touch presses
|
|
6126
6171
|
let inputLastTouchTime = -1e9; // when a touch event last came, the mouse events a browser makes from a tap follow it
|
|
6127
6172
|
|
|
@@ -6164,6 +6209,7 @@ function inputClear()
|
|
|
6164
6209
|
if (!i || inputData[i]) inputData[i] = [];
|
|
6165
6210
|
inputKeysHeld.clear();
|
|
6166
6211
|
inputWasTouching = 0;
|
|
6212
|
+
inputPinch = undefined;
|
|
6167
6213
|
// release the touch gamepad, a finger still on it has to lift before it can take a control again
|
|
6168
6214
|
touchGamepadPointerRole.clear();
|
|
6169
6215
|
touchGamepadButtons.length = 0;
|
|
@@ -6691,19 +6737,52 @@ function inputInit()
|
|
|
6691
6737
|
const pressTouch = !inputWasTouching && e.type == 'touchstart' &&
|
|
6692
6738
|
[...e.changedTouches].find(t=> !isGamepadTouch(t));
|
|
6693
6739
|
const touch = pressTouch || gameTouches[0];
|
|
6740
|
+
let pos = vec2(touch.clientX, touch.clientY);
|
|
6741
|
+
|
|
6742
|
+
// two fingers pinch: how far they moved together or apart is the touch screen's wheel, a
|
|
6743
|
+
// click for every 50 pixels, and the mouse is between them, the place to zoom about; with
|
|
6744
|
+
// touchPinchWheel off the pinch is only measured, and the first finger is the mouse as ever
|
|
6745
|
+
const pinching = touching > 1 && touchPinchWheel;
|
|
6746
|
+
if (touching > 1)
|
|
6747
|
+
{
|
|
6748
|
+
const [a, b] = gameTouches, key = a.identifier + ' ' + b.identifier;
|
|
6749
|
+
const distance = hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
6750
|
+
if (inputPinch && inputPinch.key === key)
|
|
6751
|
+
{
|
|
6752
|
+
const delta = (inputPinch.distance - distance) / 50;
|
|
6753
|
+
touchPinch += delta;
|
|
6754
|
+
if (touchPinchWheel)
|
|
6755
|
+
mouseWheel += delta;
|
|
6756
|
+
}
|
|
6757
|
+
inputPinch = {key, distance}; // other fingers start it over, with no jump
|
|
6758
|
+
if (pinching)
|
|
6759
|
+
pos = vec2((a.clientX + b.clientX) / 2, (a.clientY + b.clientY) / 2);
|
|
6760
|
+
}
|
|
6761
|
+
else
|
|
6762
|
+
inputPinch = undefined;
|
|
6694
6763
|
|
|
6695
6764
|
// set event pos and pass it along
|
|
6696
|
-
const pos = vec2(touch.clientX, touch.clientY);
|
|
6697
6765
|
const mousePosScreenLast = mousePosScreen;
|
|
6698
6766
|
mousePosScreen = mouseEventToScreen(pos);
|
|
6699
|
-
if (
|
|
6767
|
+
if (pinching)
|
|
6768
|
+
{
|
|
6769
|
+
// a pinch is not a press or a drag, the finger that pressed lets go
|
|
6770
|
+
if (inputData[0][button] & 1)
|
|
6771
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
6772
|
+
}
|
|
6773
|
+
else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
6700
6774
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
6701
6775
|
else if (pressTouch)
|
|
6702
6776
|
inputData[0][button] = 3;
|
|
6703
|
-
|
|
6777
|
+
// the finger left after a pinch moves the mouse from where it is, with no jump and no press
|
|
6778
|
+
inputTouchIdentifier = pinching ? undefined : touch.identifier;
|
|
6779
|
+
}
|
|
6780
|
+
else
|
|
6781
|
+
{
|
|
6782
|
+
inputPinch = undefined;
|
|
6783
|
+
if (inputWasTouching && (inputData[0][button] & 1))
|
|
6784
|
+
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
6704
6785
|
}
|
|
6705
|
-
else if (inputWasTouching && (inputData[0][button] & 1))
|
|
6706
|
-
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
6707
6786
|
|
|
6708
6787
|
// set was touching
|
|
6709
6788
|
inputWasTouching = touching;
|
|
@@ -6962,7 +7041,7 @@ function inputUpdatePost()
|
|
|
6962
7041
|
for (const deviceInputData of inputData)
|
|
6963
7042
|
for (const i in deviceInputData)
|
|
6964
7043
|
deviceInputData[i] &= 1;
|
|
6965
|
-
mouseWheel = 0;
|
|
7044
|
+
mouseWheel = touchPinch = 0;
|
|
6966
7045
|
mouseDelta = vec2();
|
|
6967
7046
|
mouseDeltaScreen = vec2();
|
|
6968
7047
|
}
|
|
@@ -7694,10 +7773,9 @@ class Sound
|
|
|
7694
7773
|
false&&ASSERT(isNumber(range), 'range must be a number');
|
|
7695
7774
|
false&&ASSERT(isNumber(taper), 'taper must be a number');
|
|
7696
7775
|
|
|
7697
|
-
/** @property {number} - World space max range of sound */
|
|
7698
7776
|
this.range = range;
|
|
7699
7777
|
/** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
|
|
7700
|
-
* to render3D.soundDefaultRange instead, which is further */
|
|
7778
|
+
* to render3D.soundDefaultRange instead, which is further; setting range makes it false */
|
|
7701
7779
|
this.rangeIsDefault = rangeIsDefault;
|
|
7702
7780
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
7703
7781
|
this.taper = taper;
|
|
@@ -7753,6 +7831,15 @@ class Sound
|
|
|
7753
7831
|
}
|
|
7754
7832
|
}
|
|
7755
7833
|
|
|
7834
|
+
/** World space max range of sound, 0 for no limit; a range that is set is the sound's own, in 3D too
|
|
7835
|
+
* @type {number} */
|
|
7836
|
+
get range() { return this.rangeValue; }
|
|
7837
|
+
set range(range)
|
|
7838
|
+
{
|
|
7839
|
+
this.rangeValue = range;
|
|
7840
|
+
this.rangeIsDefault = false;
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7756
7843
|
/** Sample data for each channel
|
|
7757
7844
|
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
7758
7845
|
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
@@ -13118,13 +13205,13 @@ class LightSystemPlugin
|
|
|
13118
13205
|
// an automatic size follows the canvas, so reallocate the lightmap when
|
|
13119
13206
|
// the canvas changed size, after the flush so the batch keeps its texture
|
|
13120
13207
|
const size = lightSystem.textureSize;
|
|
13121
|
-
|
|
13122
|
-
|
|
13208
|
+
const wantX = glClampTextureSize(mainCanvasSize.x), wantY = glClampTextureSize(mainCanvasSize.y);
|
|
13209
|
+
if (lightSystem.textureSizeAuto && (size.x !== wantX || size.y !== wantY))
|
|
13123
13210
|
{
|
|
13124
|
-
lightSystem.textureSize =
|
|
13211
|
+
lightSystem.textureSize = vec2(wantX, wantY);
|
|
13125
13212
|
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
13126
13213
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
|
|
13127
|
-
|
|
13214
|
+
wantX, wantY, 0,
|
|
13128
13215
|
glContext.RGBA, glContext.UNSIGNED_BYTE, null);
|
|
13129
13216
|
// put back the texture the engine tracks, a draw in renderLight must not sample the lightmap
|
|
13130
13217
|
if (glActiveTexture)
|
|
@@ -13264,8 +13351,10 @@ class LightSystemPlugin
|
|
|
13264
13351
|
this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
|
|
13265
13352
|
this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
|
|
13266
13353
|
const size = this.textureSize;
|
|
13267
|
-
if (size)
|
|
13268
|
-
|
|
13354
|
+
if (!size) return;
|
|
13355
|
+
const x = glClampTextureSize(size.x), y = glClampTextureSize(size.y);
|
|
13356
|
+
if (x !== size.x || y !== size.y)
|
|
13357
|
+
this.textureSize = vec2(x, y); // only when it is too large, this runs every frame
|
|
13269
13358
|
}
|
|
13270
13359
|
|
|
13271
13360
|
/** Draw a single Light's falloff blob into the currently bound lightmap.
|
|
@@ -20103,6 +20192,179 @@ function sceneUpdate() { paused || !timeScale || sceneCurrent?.update?.(); }
|
|
|
20103
20192
|
function sceneRender() { sceneCurrent?.render?.(); }
|
|
20104
20193
|
function sceneRenderPost() { sceneCurrent?.renderPost?.(); }
|
|
20105
20194
|
|
|
20195
|
+
/**
|
|
20196
|
+
* LittleJS Parallax Plugin
|
|
20197
|
+
* - A background layer that follows the camera by a part of its movement, so it looks far away
|
|
20198
|
+
* - Repeats across the view, so a level of any width has a background
|
|
20199
|
+
* - Draws mountains by default, or anything a game draws into it
|
|
20200
|
+
* @namespace Parallax
|
|
20201
|
+
*/
|
|
20202
|
+
|
|
20203
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
20204
|
+
|
|
20205
|
+
/**
|
|
20206
|
+
* A background layer with parallax: an image that follows the camera by a part of its movement and repeats across
|
|
20207
|
+
* the view, drawn once into a canvas by a function, mountains when none is given
|
|
20208
|
+
* - A far layer has a parallax near 1 and a low renderOrder, a near one a smaller parallax and a higher renderOrder
|
|
20209
|
+
* - The image should meet itself at its left and right edges to repeat without a seam, as the mountains do
|
|
20210
|
+
* @memberof Parallax
|
|
20211
|
+
* @extends CanvasLayer
|
|
20212
|
+
* @example
|
|
20213
|
+
* // three ranges of mountains, the far ones lighter and slower
|
|
20214
|
+
* for (let i = 3; i--;)
|
|
20215
|
+
* new ParallaxLayer(vec2(0, i*2), vec2(40, 20), .9 - i*.2, -1e3 + i,
|
|
20216
|
+
* parallaxMountains(hsl(.6, .3, .7 - i*.2), hsl(.6, .5, .2), i));
|
|
20217
|
+
*
|
|
20218
|
+
* // or draw your own image into the layer
|
|
20219
|
+
* new ParallaxLayer(vec2(), vec2(40, 20), .5, -1e3, (context, size)=>
|
|
20220
|
+
* {
|
|
20221
|
+
* context.fillStyle = '#fff';
|
|
20222
|
+
* context.fillRect(size.x/2 - 20, size.y/2 - 20, 40, 40);
|
|
20223
|
+
* });
|
|
20224
|
+
*/
|
|
20225
|
+
class ParallaxLayer extends CanvasLayer
|
|
20226
|
+
{
|
|
20227
|
+
/** Create a parallax layer and draw its image
|
|
20228
|
+
* @param {Vector2} [pos] - Where the middle of the image is in the world when the camera is there too
|
|
20229
|
+
* @param {Vector2} [size] - World size of one copy of the image
|
|
20230
|
+
* @param {number|Vector2} [parallax] - How much of the camera's movement it follows, 0 stays with the world
|
|
20231
|
+
* and 1 with the screen, a Vector2 to follow x and y by different amounts
|
|
20232
|
+
* @param {number} [renderOrder] - Low to draw behind the game, far layers lowest
|
|
20233
|
+
* @param {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} [drawFunction] - Draws
|
|
20234
|
+
* the image, given the canvas context, its size in pixels and the layer; mountains when not given
|
|
20235
|
+
* @param {Vector2} [canvasSize] - Size of the image in pixels */
|
|
20236
|
+
constructor(pos=vec2(), size=vec2(32, 16), parallax=.5, renderOrder=-1e3, drawFunction=parallaxMountains(),
|
|
20237
|
+
canvasSize=vec2(512, 256))
|
|
20238
|
+
{
|
|
20239
|
+
false&&ASSERT(isNumber(parallax) || isVector2(parallax), 'parallax must be a number or a Vector2');
|
|
20240
|
+
false&&ASSERT(typeof drawFunction == 'function', 'drawFunction must be a function');
|
|
20241
|
+
super(pos, size, 0, renderOrder, canvasSize);
|
|
20242
|
+
|
|
20243
|
+
/** @property {Vector2} - How much of the camera's movement it follows on each axis, 0 stays with the world
|
|
20244
|
+
* and 1 with the screen */
|
|
20245
|
+
this.parallax = isVector2(parallax) ? /** @type {Vector2} */ (parallax).copy() :
|
|
20246
|
+
vec2(/** @type {number} */ (parallax));
|
|
20247
|
+
/** @property {boolean} - Repeat the image across the view, left and right */
|
|
20248
|
+
this.wrapX = true;
|
|
20249
|
+
/** @property {boolean} - Repeat the image up and down the view */
|
|
20250
|
+
this.wrapY = false;
|
|
20251
|
+
/** @property {number} - How much the camera's zoom changes its size on the screen: 1 like the world, 0 not
|
|
20252
|
+
* at all, the same on the screen at any zoom as it is at zoomScale */
|
|
20253
|
+
this.zoomFollow = 1;
|
|
20254
|
+
/** @property {number} - The camera scale its size is given for, used when zoomFollow is under 1 */
|
|
20255
|
+
this.zoomScale = cameraScale;
|
|
20256
|
+
/** @property {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} - Draws the
|
|
20257
|
+
* image, redraw() after changing it */
|
|
20258
|
+
this.drawFunction = drawFunction;
|
|
20259
|
+
this.redraw();
|
|
20260
|
+
}
|
|
20261
|
+
|
|
20262
|
+
/** Draw the image again with drawFunction, after changing it or what it draws from */
|
|
20263
|
+
redraw()
|
|
20264
|
+
{
|
|
20265
|
+
const context = this.context;
|
|
20266
|
+
if (!context) return; // headless
|
|
20267
|
+
const size = vec2(this.canvas.width, this.canvas.height);
|
|
20268
|
+
context.clearRect(0, 0, size.x, size.y);
|
|
20269
|
+
this.drawFunction(context, size, this);
|
|
20270
|
+
this.updateWebGL();
|
|
20271
|
+
}
|
|
20272
|
+
|
|
20273
|
+
// how much smaller it is drawn than its size, from the camera's zoom and how much of it the layer follows
|
|
20274
|
+
zoomFactor()
|
|
20275
|
+
{ return this.zoomFollow < 1 && cameraScale > 0 ? (this.zoomScale / cameraScale) ** (1 - this.zoomFollow) : 1; }
|
|
20276
|
+
|
|
20277
|
+
/** World size one copy of the image is drawn at now, its size unless zoomFollow is under 1
|
|
20278
|
+
* @return {Vector2} */
|
|
20279
|
+
getDrawSize() { return this.size.scale(this.zoomFactor()); }
|
|
20280
|
+
|
|
20281
|
+
/** Where each copy of the image is drawn now, in the world: one place, or with wrapping every place that
|
|
20282
|
+
* shows in the view
|
|
20283
|
+
* @return {Array<Vector2>} */
|
|
20284
|
+
getDrawPositions()
|
|
20285
|
+
{
|
|
20286
|
+
// its place seen from the camera is cut down by the parallax, and by the zoom it does not follow
|
|
20287
|
+
const zoom = this.zoomFactor(), size = this.size.scale(zoom), p = this.parallax;
|
|
20288
|
+
const offset = this.pos.subtract(cameraPos);
|
|
20289
|
+
const center = cameraPos.add(vec2(offset.x * (1 - p.x) * zoom, offset.y * (1 - p.y) * zoom));
|
|
20290
|
+
|
|
20291
|
+
// the copies that reach into the view; a turned camera sees as far as its corner each way
|
|
20292
|
+
const view = mainCanvasSize.scale(.5 / (cameraScale || 1));
|
|
20293
|
+
const reach = cameraAngle ? vec2(view.length()) : view;
|
|
20294
|
+
const range = (wrap, axis)=>
|
|
20295
|
+
{
|
|
20296
|
+
if (!wrap || !(size[axis] > 0)) return [0, 0];
|
|
20297
|
+
const from = cameraPos[axis] - reach[axis] - size[axis] / 2 - center[axis];
|
|
20298
|
+
const to = cameraPos[axis] + reach[axis] + size[axis] / 2 - center[axis];
|
|
20299
|
+
const first = ceil(from / size[axis]);
|
|
20300
|
+
return [first, min(floor(to / size[axis]), first + 99)]; // a tiny image does not draw without end
|
|
20301
|
+
};
|
|
20302
|
+
const [x0, x1] = range(this.wrapX, 'x'), [y0, y1] = range(this.wrapY, 'y'), positions = [];
|
|
20303
|
+
for (let y = y0; y <= y1; ++y)
|
|
20304
|
+
for (let x = x0; x <= x1; ++x)
|
|
20305
|
+
positions.push(vec2(center.x + x * size.x, center.y + y * size.y));
|
|
20306
|
+
return positions;
|
|
20307
|
+
}
|
|
20308
|
+
|
|
20309
|
+
// Render the layer, called automatically by the engine
|
|
20310
|
+
render()
|
|
20311
|
+
{
|
|
20312
|
+
const size = this.getDrawSize();
|
|
20313
|
+
for (const pos of this.getDrawPositions())
|
|
20314
|
+
this.draw(pos, size, this.color, 0, this.mirror, this.additiveColor, false);
|
|
20315
|
+
}
|
|
20316
|
+
}
|
|
20317
|
+
|
|
20318
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
20319
|
+
|
|
20320
|
+
// the ridge of a range of mountains: a height for each column of an image and one more, the last the same as the
|
|
20321
|
+
// first so the image repeats with no step; it wanders about the middle, turning now and then
|
|
20322
|
+
function parallaxRidge(width, height, random)
|
|
20323
|
+
{
|
|
20324
|
+
const pointiness = .2, levelness = .005, slopeRange = height / 256; // how often it turns, how hard it is
|
|
20325
|
+
const ridge = new Float32Array(width + 1), middle = height / 2; // pulled level, how steep it gets
|
|
20326
|
+
let y = middle, slope = random.float(-slopeRange, slopeRange);
|
|
20327
|
+
for (let x = 0; x <= width; ++x)
|
|
20328
|
+
{
|
|
20329
|
+
ridge[x] = y;
|
|
20330
|
+
y += slope -= (y - middle) * levelness;
|
|
20331
|
+
if (random.float() < pointiness)
|
|
20332
|
+
slope = random.float(-slopeRange, slopeRange);
|
|
20333
|
+
}
|
|
20334
|
+
// take out the difference between its ends, a little from each column, and keep it inside the image
|
|
20335
|
+
const drift = ridge[width] - ridge[0];
|
|
20336
|
+
for (let x = 0; x <= width; ++x)
|
|
20337
|
+
ridge[x] = clamp(ridge[x] - drift * x / width, 0, height);
|
|
20338
|
+
return ridge;
|
|
20339
|
+
}
|
|
20340
|
+
|
|
20341
|
+
/** A draw function for a ParallaxLayer: a range of mountains across the image, shaded from the color of their
|
|
20342
|
+
* peaks down to the color of their feet, that repeats with no seam
|
|
20343
|
+
* @param {Color} [topColor] - Color at the peaks
|
|
20344
|
+
* @param {Color} [bottomColor] - Color at the bottom of the image
|
|
20345
|
+
* @param {number} [seed] - The same seed gives the same mountains, random when not given
|
|
20346
|
+
* @return {function(OffscreenCanvasRenderingContext2D, Vector2): void}
|
|
20347
|
+
* @memberof Parallax */
|
|
20348
|
+
function parallaxMountains(topColor=hsl(.6, .2, .6), bottomColor=hsl(.6, .3, .25), seed)
|
|
20349
|
+
{
|
|
20350
|
+
return (context, size)=>
|
|
20351
|
+
{
|
|
20352
|
+
const w = size.x, h = size.y;
|
|
20353
|
+
const random = new RandomGenerator(seed === undefined ? randInt(1e9) + 1 : seed * 7919 + 1);
|
|
20354
|
+
const ridge = parallaxRidge(w, h, random);
|
|
20355
|
+
|
|
20356
|
+
// a strip one pixel wide with the shading, stretched down each column from its peak
|
|
20357
|
+
const strip = createCanvasContext(1, h);
|
|
20358
|
+
for (let i = h; i--;)
|
|
20359
|
+
{
|
|
20360
|
+
strip.fillStyle = topColor.lerp(bottomColor, i / h).toString();
|
|
20361
|
+
strip.fillRect(0, i, 1, 1);
|
|
20362
|
+
}
|
|
20363
|
+
for (let x = w; x--;)
|
|
20364
|
+
ridge[x] < h && context.drawImage(strip.canvas, 0, 0, 1, h, x, ridge[x], 1, h - ridge[x]);
|
|
20365
|
+
};
|
|
20366
|
+
}
|
|
20367
|
+
|
|
20106
20368
|
/**
|
|
20107
20369
|
* LittleJS PathFinder Plugin
|
|
20108
20370
|
* - Grid-based A* pathfinder with two-pass smoothing for natural-looking paths
|
|
@@ -22369,12 +22631,17 @@ function render3DShadowCaster()
|
|
|
22369
22631
|
light.intensity > 0 && light.color.a > 0 ? light : undefined;
|
|
22370
22632
|
}
|
|
22371
22633
|
|
|
22634
|
+
// the widest cone a shadow map can look down, to each side; the light that casts the shadows lights no wider, or
|
|
22635
|
+
// what is past the map's edge would be lit with nothing to shade it
|
|
22636
|
+
const RENDER3D_SHADOW_CONE_MAX = 1.35;
|
|
22637
|
+
|
|
22372
22638
|
// a light's cone as the four numbers the shader gets: the way it shines, scaled so that its dot with the way to
|
|
22373
22639
|
// a point, less the fourth number, is 0 at the edge of the cone and 1 where its fade starts; a light with no cone
|
|
22374
22640
|
// gets numbers that make it 1 every way
|
|
22375
22641
|
function render3DLightCone(light)
|
|
22376
22642
|
{
|
|
22377
|
-
const
|
|
22643
|
+
const widest = light === render3DShadowCaster() ? RENDER3D_SHADOW_CONE_MAX : PI;
|
|
22644
|
+
const angle = light.directional ? 0 : min(light.coneAngle, widest);
|
|
22378
22645
|
if (!(angle > 0)) return [0, 0, 0, -1];
|
|
22379
22646
|
const outer = cos(angle), inner = cos(angle * (1 - clamp(light.coneSoftness)));
|
|
22380
22647
|
const k = 1 / max(inner - outer, 1e-4), forward = light.getForward3D();
|
|
@@ -23161,8 +23428,9 @@ class Render3DPlugin
|
|
|
23161
23428
|
// a spotlight's map looks down its cone with perspective, from just in front of it out to its radius
|
|
23162
23429
|
const pos = caster.getWorldPos3D(), forward = caster.getForward3D();
|
|
23163
23430
|
const up = abs(forward.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0);
|
|
23164
|
-
|
|
23165
|
-
const
|
|
23431
|
+
// the near plane stays in front of the far one however small the light
|
|
23432
|
+
const far = caster.radius, near = min(max(far / 500, .02), far / 2);
|
|
23433
|
+
const fov = min(caster.coneAngle, RENDER3D_SHADOW_CONE_MAX) * 2 + .1;
|
|
23166
23434
|
this.shadowMatrix = Matrix4.perspective(fov, 1, near, far).multiply(
|
|
23167
23435
|
Matrix4.lookAt(pos, pos.add(forward), up).invert());
|
|
23168
23436
|
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
@@ -26596,7 +26864,7 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
26596
26864
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
26597
26865
|
* - radius is a world distance, so scale3D does not change it
|
|
26598
26866
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
26599
|
-
* - Draws nothing
|
|
26867
|
+
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
26600
26868
|
* @extends EngineObject3D
|
|
26601
26869
|
* @memberof Render3D
|
|
26602
26870
|
* @example
|
|
@@ -26645,8 +26913,10 @@ class Light3D extends EngineObject3D
|
|
|
26645
26913
|
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
26646
26914
|
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
26647
26915
|
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
26648
|
-
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
26649
|
-
|
|
26916
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
|
|
26917
|
+
// a browser that can not make the glow's texture draws no glow, not a plain square
|
|
26918
|
+
if (texture || !glContext)
|
|
26919
|
+
r.drawBillboard(at, vec2(this.glow), texture, color);
|
|
26650
26920
|
}
|
|
26651
26921
|
}
|
|
26652
26922
|
|
|
@@ -26656,7 +26926,7 @@ class Light3D extends EngineObject3D
|
|
|
26656
26926
|
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
26657
26927
|
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
26658
26928
|
* - It cannot sit on the origin, since that leaves no direction
|
|
26659
|
-
* -
|
|
26929
|
+
* - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
|
|
26660
26930
|
* @extends Light3D
|
|
26661
26931
|
* @memberof Render3D
|
|
26662
26932
|
* @example
|
|
@@ -27733,6 +28003,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27733
28003
|
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
27734
28004
|
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
27735
28005
|
this.collideLevel = false;
|
|
28006
|
+
/** @property {number} - How much a particle grips where it lands, 0 to 1: its speed along the surface is
|
|
28007
|
+
* cut by this much on each hit, on top of the friction, 1 stops it there */
|
|
28008
|
+
this.stick = 0;
|
|
27736
28009
|
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
27737
28010
|
* @type {Particle3DCallback|undefined} */
|
|
27738
28011
|
this.particleCreateCallback = undefined;
|
|
@@ -27752,7 +28025,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27752
28025
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
27753
28026
|
* @type {Vector3|undefined} */
|
|
27754
28027
|
this.worldPos3D = undefined;
|
|
27755
|
-
this.emitTimeBuffer =
|
|
28028
|
+
this.emitTimeBuffer = 1; // the first particle comes at once, as the 2D emitter's does
|
|
27756
28029
|
}
|
|
27757
28030
|
|
|
27758
28031
|
/** Spawn new particles, move the live ones, and go away when done */
|
|
@@ -27769,9 +28042,10 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27769
28042
|
{
|
|
27770
28043
|
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
27771
28044
|
// neither is a reason to stop counting down the emit time
|
|
27772
|
-
|
|
28045
|
+
const rate = this.emitRate * particleEmitRateScale;
|
|
28046
|
+
if (rate > 0 && rate < Infinity)
|
|
27773
28047
|
{
|
|
27774
|
-
this.emitTimeBuffer +=
|
|
28048
|
+
this.emitTimeBuffer += rate * timeDelta;
|
|
27775
28049
|
for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
|
|
27776
28050
|
this.emitParticle();
|
|
27777
28051
|
}
|
|
@@ -27811,7 +28085,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27811
28085
|
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
27812
28086
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
27813
28087
|
data[k+18] += data[k+19] *= angleDamping;
|
|
27814
|
-
|
|
28088
|
+
// true when its collide callback destroyed it, it has had its last callback but the destroy one
|
|
28089
|
+
const gone = collideLevel && this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz);
|
|
28090
|
+
collideLevel && reread();
|
|
27815
28091
|
const t = i * trailMax * 3;
|
|
27816
28092
|
if (trailMax)
|
|
27817
28093
|
{
|
|
@@ -27823,7 +28099,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27823
28099
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
27824
28100
|
data[k+20] = n + 1;
|
|
27825
28101
|
}
|
|
27826
|
-
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
28102
|
+
updateCallback && !gone && (this.particleCall(updateCallback, k), reread());
|
|
27827
28103
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
27828
28104
|
{
|
|
27829
28105
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
@@ -27925,13 +28201,14 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27925
28201
|
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
27926
28202
|
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
27927
28203
|
if (view.destroyed)
|
|
27928
|
-
out[k+17] = max(out[k+17], out[k+16]
|
|
28204
|
+
out[k+17] = max(out[k+17], out[k+16]); // its life lived: a step short of it can round to just under
|
|
27929
28205
|
return result;
|
|
27930
28206
|
}
|
|
27931
28207
|
|
|
27932
28208
|
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
27933
28209
|
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
27934
|
-
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
28210
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is; true when the callback
|
|
28211
|
+
// destroyed it
|
|
27935
28212
|
particleCollide(k, x, y, z)
|
|
27936
28213
|
{
|
|
27937
28214
|
let data = this.particleData;
|
|
@@ -27944,18 +28221,21 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
27944
28221
|
if (h && (!hit || h.distance < hit.distance))
|
|
27945
28222
|
hit = h, level = l;
|
|
27946
28223
|
}
|
|
27947
|
-
if (!hit) return;
|
|
28224
|
+
if (!hit) return false;
|
|
27948
28225
|
const point = from.lerp(to, hit.distance);
|
|
27949
28226
|
const callback = this.particleCollideCallback;
|
|
27950
28227
|
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
27951
|
-
return; // let through, or destroyed by the callback
|
|
28228
|
+
return this.particleView.destroyed; // let through, or destroyed by the callback
|
|
27952
28229
|
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
27953
28230
|
|
|
27954
28231
|
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
27955
|
-
|
|
28232
|
+
// the larger friction of the two, as objects take it, then the emitter's own grip, which a level can not undo
|
|
28233
|
+
const restitution = max(this.restitution, level.restitution);
|
|
28234
|
+
const friction = max(this.friction, level.friction) * (1 - clamp(this.stick));
|
|
27956
28235
|
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
27957
28236
|
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
27958
28237
|
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
28238
|
+
return false;
|
|
27959
28239
|
}
|
|
27960
28240
|
|
|
27961
28241
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -28219,6 +28499,11 @@ class LensFlare3D extends EngineObject3D
|
|
|
28219
28499
|
* height; shape is glow, disc or ring
|
|
28220
28500
|
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
28221
28501
|
this.elements = undefined;
|
|
28502
|
+
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
28503
|
+
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
28504
|
+
* in front of the light, and a spotlight's shows from inside its beam only
|
|
28505
|
+
* @type {Light3D|undefined} */
|
|
28506
|
+
this.light = undefined;
|
|
28222
28507
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
28223
28508
|
this.occlusion = true;
|
|
28224
28509
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
@@ -28257,13 +28542,30 @@ class LensFlare3D extends EngineObject3D
|
|
|
28257
28542
|
return this.made;
|
|
28258
28543
|
}
|
|
28259
28544
|
|
|
28260
|
-
|
|
28545
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun, and a point
|
|
28546
|
+
// to find it on the screen by; undefined with no sun direction, or a light that is gone or at the camera
|
|
28547
|
+
flareSource()
|
|
28548
|
+
{
|
|
28549
|
+
const camera = render3D.camera.pos, light = this.light;
|
|
28550
|
+
if (!light)
|
|
28551
|
+
{
|
|
28552
|
+
const sun = render3D.sunDirection;
|
|
28553
|
+
if (!sun.lengthSquared()) return;
|
|
28554
|
+
const direction = sun.normalize();
|
|
28555
|
+
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
28556
|
+
}
|
|
28557
|
+
if (light.destroyed) return;
|
|
28558
|
+
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
28559
|
+
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
28560
|
+
}
|
|
28561
|
+
|
|
28562
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
28563
|
+
* behind the camera
|
|
28261
28564
|
* @return {Vector2|undefined} */
|
|
28262
28565
|
getSunScreenPos()
|
|
28263
28566
|
{
|
|
28264
|
-
const
|
|
28265
|
-
|
|
28266
|
-
return render3D.worldToScreen(render3D.camera.pos.add(direction.normalize(100)));
|
|
28567
|
+
const source = this.flareSource();
|
|
28568
|
+
return source && render3D.worldToScreen(source.pos);
|
|
28267
28569
|
}
|
|
28268
28570
|
|
|
28269
28571
|
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
@@ -28277,7 +28579,19 @@ class LensFlare3D extends EngineObject3D
|
|
|
28277
28579
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
28278
28580
|
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
28279
28581
|
if (!(strength > 0)) return [];
|
|
28280
|
-
|
|
28582
|
+
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
28583
|
+
const light = this.light, source = light && this.flareSource();
|
|
28584
|
+
if (light)
|
|
28585
|
+
{
|
|
28586
|
+
if (!source) return [];
|
|
28587
|
+
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
28588
|
+
const cone = render3DLightCone(light), d = source.direction;
|
|
28589
|
+
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
28590
|
+
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
28591
|
+
if (!(bright > 0)) return [];
|
|
28592
|
+
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
28593
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
28594
|
+
}
|
|
28281
28595
|
return this.getElements().map((e)=>
|
|
28282
28596
|
{
|
|
28283
28597
|
const c = e.color.multiply(tint);
|
|
@@ -28287,15 +28601,32 @@ class LensFlare3D extends EngineObject3D
|
|
|
28287
28601
|
});
|
|
28288
28602
|
}
|
|
28289
28603
|
|
|
28290
|
-
/** Is something between the camera and the sun: the level, or an object that is not see
|
|
28604
|
+
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
28605
|
+
* through
|
|
28291
28606
|
* @return {boolean} */
|
|
28292
28607
|
isSunHidden()
|
|
28293
28608
|
{
|
|
28294
|
-
const
|
|
28295
|
-
if (!
|
|
28296
|
-
const
|
|
28297
|
-
|
|
28298
|
-
|
|
28609
|
+
const source = this.flareSource();
|
|
28610
|
+
if (!source) return true;
|
|
28611
|
+
const ray = new Ray3D(render3D.camera.pos, source.direction), reach = source.distance;
|
|
28612
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.destroyed &&
|
|
28613
|
+
!o.transparent && !o.additive);
|
|
28614
|
+
// a voxel map says block by block what is see through: glass, water and leaves let the sun by, and the
|
|
28615
|
+
// ray goes on to the blocks behind them
|
|
28616
|
+
const maps = /** @type {Array<VoxelMap>} */ (typeof VoxelMap == 'undefined' ? [] :
|
|
28617
|
+
blockers.filter((o)=> o instanceof VoxelMap));
|
|
28618
|
+
for (const map of maps)
|
|
28619
|
+
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
28620
|
+
return true;
|
|
28621
|
+
// an object is hit by its box, and a box the camera is inside, a room, a wide floor or the player's own
|
|
28622
|
+
// body, says nothing of what is in the way: the ray the other way hits it too, and it is left out; so is
|
|
28623
|
+
// one the light is inside, its lamp
|
|
28624
|
+
const backward = ray.direction.scale(-1), back = new Ray3D(ray.origin, backward);
|
|
28625
|
+
const lamp = this.light && [new Ray3D(source.pos, ray.direction), new Ray3D(source.pos, backward)];
|
|
28626
|
+
const hits = (r, o)=> render3DRaycastObject(r, o) !== undefined;
|
|
28627
|
+
const around = (o)=> !(o instanceof HeightMap) && (hits(back, o) || lamp && hits(lamp[0], o) && hits(lamp[1], o));
|
|
28628
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !around(o)));
|
|
28629
|
+
return !!hit && hit.distance < reach;
|
|
28299
28630
|
}
|
|
28300
28631
|
|
|
28301
28632
|
/** Ease visible toward whether the sun shows, called automatically each frame */
|
|
@@ -29425,7 +29756,10 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
29425
29756
|
* returned, see level3DVoxelSetup for its sheet
|
|
29426
29757
|
* - A level may hold a terrain, in a terrain block: pos, its center, size, its size in the world along x and z,
|
|
29427
29758
|
* 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
|
|
29428
|
-
* is made a HeightMap, returned with what else was made
|
|
29759
|
+
* is made a HeightMap, returned with what else was made; paint, when it has it, colors its samples: colors, a
|
|
29760
|
+
* 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
|
|
29761
|
+
* - A level may hold prefabs of its own, in a prefabs block, each by its name as level3DAddPrefab takes it; they
|
|
29762
|
+
* are added before its objects are made, one the game added itself keeps its place
|
|
29429
29763
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
29430
29764
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
29431
29765
|
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
|
|
@@ -29438,6 +29772,7 @@ function level3DLoad(level)
|
|
|
29438
29772
|
false&&ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
29439
29773
|
false&&ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
29440
29774
|
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
29775
|
+
level3DPrefabsAdd(level.prefabs);
|
|
29441
29776
|
level3DSceneApply(level.scene);
|
|
29442
29777
|
const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
|
|
29443
29778
|
map && made.push(map);
|
|
@@ -29548,14 +29883,49 @@ function level3DTerrainShape(terrain)
|
|
|
29548
29883
|
height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
29549
29884
|
}
|
|
29550
29885
|
|
|
29886
|
+
// a hex color of a level as a Color, undefined when it is not one
|
|
29887
|
+
function level3DHexColor(hex)
|
|
29888
|
+
{ return /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex) ? new Color().setHex(hex) : undefined; }
|
|
29889
|
+
|
|
29890
|
+
// the paint of a level's terrain block, for a terrain of so many samples: its colors as they are written and a
|
|
29891
|
+
// color for each sample, row after row, 0 for none and 1 the first of the colors; undefined when it has none or
|
|
29892
|
+
// it does not fit the terrain
|
|
29893
|
+
function level3DTerrainPaint(terrain, samples)
|
|
29894
|
+
{
|
|
29895
|
+
const paint = terrain?.paint, colors = paint?.colors, runs = paint?.cells;
|
|
29896
|
+
if (!isArray(colors) || !isArray(runs) || colors.length > 255 || !colors.every(level3DHexColor)) return;
|
|
29897
|
+
const cells = new Uint8Array(samples);
|
|
29898
|
+
let at = 0;
|
|
29899
|
+
for (let i = 0; i < runs.length; i += 2)
|
|
29900
|
+
{
|
|
29901
|
+
const count = runs[i], color = runs[i + 1];
|
|
29902
|
+
if (!(count > 0 && count % 1 === 0 && color >= 0 && color % 1 === 0) || color > colors.length ||
|
|
29903
|
+
at + count > samples) return;
|
|
29904
|
+
cells.fill(color, at, at += count);
|
|
29905
|
+
}
|
|
29906
|
+
return at === samples ? {colors: colors.slice(), cells} : undefined;
|
|
29907
|
+
}
|
|
29908
|
+
|
|
29909
|
+
// give a terrain's map the colors of its block: its own color over the whole of it, or with paint a color for
|
|
29910
|
+
// each sample, the terrain's own where it is not painted; the caller builds the map again
|
|
29911
|
+
function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain, map.rows * map.columns))
|
|
29912
|
+
{
|
|
29913
|
+
const base = level3DHexColor(terrain.color) || WHITE;
|
|
29914
|
+
map.color = base, map.colors = undefined;
|
|
29915
|
+
if (!paint || !paint.cells.some((v)=> v)) return;
|
|
29916
|
+
const palette = [base, ...paint.colors.map(level3DHexColor)];
|
|
29917
|
+
map.color = WHITE;
|
|
29918
|
+
map.colors = map.heights.map((row, r)=> row.map((v, c)=> palette[paint.cells[r * map.columns + c]] || base));
|
|
29919
|
+
}
|
|
29920
|
+
|
|
29551
29921
|
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
29552
29922
|
function level3DTerrainMake(terrain)
|
|
29553
29923
|
{
|
|
29554
29924
|
const shape = level3DTerrainShape(terrain);
|
|
29555
29925
|
if (!shape || typeof HeightMap == 'undefined') return;
|
|
29556
29926
|
const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
|
|
29557
|
-
|
|
29558
|
-
|
|
29927
|
+
level3DTerrainSetColors(map, terrain);
|
|
29928
|
+
map.colors && map.rebuild();
|
|
29559
29929
|
return map;
|
|
29560
29930
|
}
|
|
29561
29931
|
|
|
@@ -29628,6 +29998,14 @@ function level3DProperties(type, object)
|
|
|
29628
29998
|
// make one object of a level from the type added for its name, undefined when there is no such type or it made
|
|
29629
29999
|
// nothing
|
|
29630
30000
|
function level3DMake(object)
|
|
30001
|
+
{
|
|
30002
|
+
return level3DMakeAt(object, level3DVector(object.pos, vec3()),
|
|
30003
|
+
level3DVector(object.rotation, vec3()).scale(PI / 180), level3DVector(object.scale, vec3(1)));
|
|
30004
|
+
}
|
|
30005
|
+
|
|
30006
|
+
// make a level object's game object at a place, a rotation in radians and a scale, its own as level3DMake gives
|
|
30007
|
+
// them or the ones a prefab's instance puts it at
|
|
30008
|
+
function level3DMakeAt(object, pos, rotation, scale)
|
|
29631
30009
|
{
|
|
29632
30010
|
const type = level3DTypes.get(object.type);
|
|
29633
30011
|
if (!type)
|
|
@@ -29635,7 +30013,7 @@ function level3DMake(object)
|
|
|
29635
30013
|
debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
|
|
29636
30014
|
return;
|
|
29637
30015
|
}
|
|
29638
|
-
const
|
|
30016
|
+
const properties = level3DProperties(type, object);
|
|
29639
30017
|
const {make} = type, count = engineObjects.length;
|
|
29640
30018
|
let result;
|
|
29641
30019
|
try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
|
|
@@ -29653,16 +30031,263 @@ function level3DMake(object)
|
|
|
29653
30031
|
if (result instanceof EngineObject3D)
|
|
29654
30032
|
{
|
|
29655
30033
|
level3DBaseScale.set(result, result.scale3D.copy());
|
|
29656
|
-
result.rotation3D =
|
|
29657
|
-
result.scale3D = result.scale3D.multiply(
|
|
30034
|
+
result.rotation3D = rotation.copy();
|
|
30035
|
+
result.scale3D = result.scale3D.multiply(scale);
|
|
29658
30036
|
}
|
|
29659
30037
|
|
|
29660
30038
|
// the type's own properties are set on it, one the type has no default for could be a field of the engine's
|
|
29661
30039
|
for (const key in type.defaults)
|
|
29662
30040
|
result[key] = properties[key];
|
|
30041
|
+
// a prefab's instance makes its parts now that it is where it goes
|
|
30042
|
+
result instanceof Prefab3D && result.placeAt(pos);
|
|
29663
30043
|
return result;
|
|
29664
30044
|
}
|
|
29665
30045
|
|
|
30046
|
+
/** Make one object of a type added with level3DAddType, level3DAddMesh or level3DAddPrefab, from code, with no
|
|
30047
|
+
* level: a prefab's instance, or a plain type
|
|
30048
|
+
* @param {string} type - The type's name
|
|
30049
|
+
* @param {Vector3} [pos3D]
|
|
30050
|
+
* @param {Vector3} [rotation3D] - In radians, as an object has it
|
|
30051
|
+
* @param {Vector3} [scale3D] - Times the scale the type makes it with
|
|
30052
|
+
* @param {Object} [properties] - Over the type's defaults
|
|
30053
|
+
* @return {any} - What the type made, a Prefab3D for a prefab, undefined when there is no such type
|
|
30054
|
+
* @memberof Level3D
|
|
30055
|
+
* @example
|
|
30056
|
+
* level3DAddPrefab('House', await fetchJSON('house.json'));
|
|
30057
|
+
* level3DSpawn('House', vec3(10, 0, 0), vec3(0, PI/2, 0)); */
|
|
30058
|
+
function level3DSpawn(type, pos3D=vec3(), rotation3D=vec3(), scale3D=vec3(1), properties)
|
|
30059
|
+
{
|
|
30060
|
+
false&&ASSERT(isVector3(pos3D) && isVector3(rotation3D) && isVector3(scale3D), 'pos3D, rotation3D and scale3D are vec3');
|
|
30061
|
+
return level3DMakeAt(properties ? {type, properties} : {type}, pos3D.copy(), rotation3D, scale3D);
|
|
30062
|
+
}
|
|
30063
|
+
|
|
30064
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
30065
|
+
// prefabs: a prefab is a small level, a list of objects about its own origin, placed many times
|
|
30066
|
+
|
|
30067
|
+
// the prefabs by name: how an instance is built, its objects, fromLevel, true for one a level's own prefabs
|
|
30068
|
+
// block added, which one the game adds takes the place of, and source, the object it was added from, which the
|
|
30069
|
+
// level editor knows a level that is a prefab by
|
|
30070
|
+
/** @type {Map<string, {attached: boolean, objects: Array<Object>, fromLevel: boolean, source: Object}>} */
|
|
30071
|
+
const level3DPrefabs = new Map;
|
|
30072
|
+
|
|
30073
|
+
// how many prefabs deep an instance being made is, a prefab that holds itself is stopped; and how many of those
|
|
30074
|
+
// are attached: a prefab inside an attached one is attached too, or its parts would be left behind in the world
|
|
30075
|
+
let level3DPrefabDepth = 0, level3DPrefabAttached = 0;
|
|
30076
|
+
|
|
30077
|
+
/** Add a prefab: a small level, its objects placed about its own origin, to place many times under one name
|
|
30078
|
+
* - It is a type from then on: a level's object of that type, the level editor's Place list and level3DSpawn make
|
|
30079
|
+
* an instance, a Prefab3D; an instance is made of the prefab as it is then, so the level editor, where a prefab
|
|
30080
|
+
* changes, makes its instances again
|
|
30081
|
+
* - The prefab is a level as the level editor saves it, {objects: [...]}, so the editor is the prefab editor too;
|
|
30082
|
+
* only its objects are used, and they may be of other prefabs
|
|
30083
|
+
* - With attached true in it the parts are children of the instance and move with it as one body, without
|
|
30084
|
+
* collision of their own, a prefab inside it too; its handle is at the middle of the box around them and its
|
|
30085
|
+
* 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
|
|
30086
|
+
* by hand does
|
|
30087
|
+
* - Adding a name again replaces it
|
|
30088
|
+
* @param {string} name - The type its instances have in a level
|
|
30089
|
+
* @param {Object} prefab - {objects, attached}
|
|
30090
|
+
* @memberof Level3D
|
|
30091
|
+
* @example
|
|
30092
|
+
* level3DAddPrefab('Tower', {objects: [{type: 'Box', pos: [0, 1, 0], scale: [2, 2, 2]},
|
|
30093
|
+
* {type: 'Cylinder', pos: [0, 3, 0]}]});
|
|
30094
|
+
* level3DLoad({objects: [{type: 'Tower', pos: [5, 0, 5]}, {type: 'Tower', pos: [-5, 0, 5], rotation: [0, 45, 0]}]}); */
|
|
30095
|
+
function level3DAddPrefab(name, prefab) { level3DPrefabSet(name, prefab, false); }
|
|
30096
|
+
|
|
30097
|
+
// add a prefab, the game's or a level's own
|
|
30098
|
+
function level3DPrefabSet(name, prefab, fromLevel)
|
|
30099
|
+
{
|
|
30100
|
+
false&&ASSERT(!!prefab && typeof prefab === 'object', 'a prefab is an object, {objects: [...]}');
|
|
30101
|
+
name = String(name);
|
|
30102
|
+
const objects = isArray(prefab?.objects) ? prefab.objects.filter((o)=> o && typeof o === 'object') : [];
|
|
30103
|
+
// a copy, what the game or the editor does to its own afterwards does not change the instances made later
|
|
30104
|
+
level3DPrefabs.set(name, {attached: !!prefab?.attached, objects: JSON.parse(JSON.stringify(objects)), fromLevel,
|
|
30105
|
+
source: prefab});
|
|
30106
|
+
// a class of its own, so the level editor makes its instances again as it does a class's
|
|
30107
|
+
level3DTypes.set(name, {make: class extends Prefab3D { constructor(pos) { super(pos, name); } }, defaults: {},
|
|
30108
|
+
tileInfo: undefined});
|
|
30109
|
+
}
|
|
30110
|
+
|
|
30111
|
+
// add a level's own prefabs, each unless the game added one of that name or it is a plain type's name
|
|
30112
|
+
function level3DPrefabsAdd(prefabs)
|
|
30113
|
+
{
|
|
30114
|
+
if (!prefabs || typeof prefabs !== 'object' || isArray(prefabs)) return;
|
|
30115
|
+
for (const name in prefabs)
|
|
30116
|
+
{
|
|
30117
|
+
const known = level3DPrefabs.get(name);
|
|
30118
|
+
if (known ? !known.fromLevel : level3DTypes.has(name)) continue;
|
|
30119
|
+
prefabs[name] && typeof prefabs[name] === 'object' && level3DPrefabSet(name, prefabs[name], true);
|
|
30120
|
+
}
|
|
30121
|
+
}
|
|
30122
|
+
|
|
30123
|
+
/** Load a prefab from a file the level editor saved and add it
|
|
30124
|
+
* @param {string} name - The type its instances have in a level
|
|
30125
|
+
* @param {string} url
|
|
30126
|
+
* @return {Promise<void>}
|
|
30127
|
+
* @memberof Level3D */
|
|
30128
|
+
async function level3DLoadPrefab(name, url) { level3DAddPrefab(name, await fetchJSON(url)); }
|
|
30129
|
+
|
|
30130
|
+
// where an instance puts one of its prefab's objects: the object's own place, turn and size inside the instance's
|
|
30131
|
+
function level3DPrefabPartTransform(instance, object)
|
|
30132
|
+
{
|
|
30133
|
+
const pos = level3DVector(object.pos, vec3()), scale = level3DVector(object.scale, vec3(1));
|
|
30134
|
+
const rotation = level3DVector(object.rotation, vec3()).scale(PI / 180);
|
|
30135
|
+
// the two turns one after the other; the sizes multiply along each axis, which is exact for an instance sized
|
|
30136
|
+
// evenly or a part that is not turned, and as near as a box can be otherwise
|
|
30137
|
+
const turn = buildMatrix(vec3(), instance.rotation3D).multiply(buildMatrix(vec3(), rotation));
|
|
30138
|
+
return {pos: buildMatrix(instance.pos3D, instance.rotation3D, instance.scale3D).transformPoint(pos),
|
|
30139
|
+
rotation: turn.getRotation(), scale: scale.multiply(instance.scale3D)};
|
|
30140
|
+
}
|
|
30141
|
+
|
|
30142
|
+
/**
|
|
30143
|
+
* An instance of a prefab, what a prefab's type makes: a handle with no shape of its own, and its parts, what the
|
|
30144
|
+
* prefab's objects made
|
|
30145
|
+
* - Parts of a prefab that is not attached are objects of their own in the world: moving, turning or sizing the
|
|
30146
|
+
* handle puts them where it now says with its next update, or call placeParts; destroying it destroys them
|
|
30147
|
+
* - Parts of an attached prefab are its children and move with it as one body
|
|
30148
|
+
* @extends EngineObject3D
|
|
30149
|
+
* @memberof Level3D
|
|
30150
|
+
*/
|
|
30151
|
+
class Prefab3D extends EngineObject3D
|
|
30152
|
+
{
|
|
30153
|
+
/** Create an instance of a prefab, made by its type: place one with a level or level3DSpawn
|
|
30154
|
+
* @param {Vector3} [pos3D]
|
|
30155
|
+
* @param {string} [prefabName] - A prefab added with level3DAddPrefab */
|
|
30156
|
+
constructor(pos3D, prefabName='')
|
|
30157
|
+
{
|
|
30158
|
+
super(pos3D);
|
|
30159
|
+
/** @property {string} - The prefab it is an instance of */
|
|
30160
|
+
this.prefabName = prefabName;
|
|
30161
|
+
/** @property {boolean} - Are its parts its children, moving with it as one body */
|
|
30162
|
+
this.attached = !!level3DPrefabs.get(prefabName)?.attached || level3DPrefabAttached > 0;
|
|
30163
|
+
/** @property {Array<any>} - What the prefab's objects made, in the prefab's order */
|
|
30164
|
+
this.parts = [];
|
|
30165
|
+
/** @property {Vector3} - From the prefab's origin to the handle, in the prefab's own space: nothing for
|
|
30166
|
+
* separate parts, and for an attached prefab the middle of the box around its parts, where its handle
|
|
30167
|
+
* is, as an object's place is the middle of its body */
|
|
30168
|
+
this.originOffset = vec3();
|
|
30169
|
+
this.partObjects = []; // the prefab's object of each part
|
|
30170
|
+
this.partsPlaced = ''; // where the handle was when its parts were last placed, undefined parts not made
|
|
30171
|
+
this.partsMade = false;
|
|
30172
|
+
}
|
|
30173
|
+
|
|
30174
|
+
/** Put the instance with its prefab's origin at a place, turned and sized as the handle is: where a level's
|
|
30175
|
+
* object or level3DSpawn says it goes; the handle of an attached prefab is then at the middle of its body
|
|
30176
|
+
* @param {Vector3} pos3D */
|
|
30177
|
+
placeAt(pos3D)
|
|
30178
|
+
{
|
|
30179
|
+
this.pos3D = pos3D.copy();
|
|
30180
|
+
this.placeParts();
|
|
30181
|
+
const o = this.originOffset;
|
|
30182
|
+
if (o.x || o.y || o.z)
|
|
30183
|
+
this.pos3D = buildMatrix(pos3D, this.rotation3D, this.scale3D).transformPoint(o);
|
|
30184
|
+
}
|
|
30185
|
+
|
|
30186
|
+
/** Put the parts where the handle is now, making them the first time; called by the handle's update when it
|
|
30187
|
+
* has moved, turned or changed size */
|
|
30188
|
+
placeParts()
|
|
30189
|
+
{
|
|
30190
|
+
if (this.destroyed) return;
|
|
30191
|
+
this.partsMade ||= (this.makeParts(), true);
|
|
30192
|
+
this.partsPlaced = this.placeKey();
|
|
30193
|
+
if (this.attached) return; // children follow by themselves
|
|
30194
|
+
this.parts.forEach((part, i)=>
|
|
30195
|
+
{
|
|
30196
|
+
if (!(part instanceof EngineObject3D)) return; // what a function made is where it was made
|
|
30197
|
+
const to = level3DPrefabPartTransform(this, this.partObjects[i]);
|
|
30198
|
+
part.rotation3D = to.rotation;
|
|
30199
|
+
part.scale3D = (level3DBaseScale.get(part) ?? vec3(1)).multiply(to.scale);
|
|
30200
|
+
// a prefab inside this one goes by its origin, which is not where an attached one's handle is
|
|
30201
|
+
part instanceof Prefab3D ? part.placeAt(to.pos) : part.pos3D = to.pos;
|
|
30202
|
+
});
|
|
30203
|
+
}
|
|
30204
|
+
|
|
30205
|
+
// the handle's place, turn and size as one text, to tell when it has changed
|
|
30206
|
+
placeKey()
|
|
30207
|
+
{
|
|
30208
|
+
const p = this.pos3D, r = this.rotation3D, s = this.scale3D;
|
|
30209
|
+
return [p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z].join();
|
|
30210
|
+
}
|
|
30211
|
+
|
|
30212
|
+
// make the parts, each where the instance puts it, or attached at its own place inside the instance
|
|
30213
|
+
makeParts()
|
|
30214
|
+
{
|
|
30215
|
+
const prefab = level3DPrefabs.get(this.prefabName);
|
|
30216
|
+
if (!prefab) return;
|
|
30217
|
+
if (level3DPrefabDepth >= 8)
|
|
30218
|
+
{
|
|
30219
|
+
debug && console.error(`level3DLoad: the prefab ${this.prefabName} holds itself, left out there`);
|
|
30220
|
+
return;
|
|
30221
|
+
}
|
|
30222
|
+
++level3DPrefabDepth;
|
|
30223
|
+
this.attached && ++level3DPrefabAttached;
|
|
30224
|
+
try
|
|
30225
|
+
{
|
|
30226
|
+
const low = vec3(Infinity), high = vec3(-Infinity), children = [];
|
|
30227
|
+
for (const object of prefab.objects)
|
|
30228
|
+
{
|
|
30229
|
+
const local = {pos: level3DVector(object.pos, vec3()), scale: level3DVector(object.scale, vec3(1)),
|
|
30230
|
+
rotation: level3DVector(object.rotation, vec3()).scale(PI / 180)};
|
|
30231
|
+
const at = this.attached ? local : level3DPrefabPartTransform(this, object);
|
|
30232
|
+
const part = level3DMakeAt(object, at.pos, at.rotation, at.scale);
|
|
30233
|
+
if (!part) continue; // level3DMakeAt gives an object or nothing
|
|
30234
|
+
this.parts.push(part);
|
|
30235
|
+
this.partObjects.push(object);
|
|
30236
|
+
if (!this.attached || !(part instanceof EngineObject3D)) continue;
|
|
30237
|
+
// a child rides with its parent and has no collision of its own
|
|
30238
|
+
part.setCollision(false, false, false);
|
|
30239
|
+
children.push(part);
|
|
30240
|
+
// the corners of its box as it is turned; its place is its middle, a prefab inside this one too
|
|
30241
|
+
const box = buildMatrix(part.pos3D, part.rotation3D, part.size3D.multiply(part.scale3D));
|
|
30242
|
+
for (let i = 8; i--;)
|
|
30243
|
+
{
|
|
30244
|
+
const p = box.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
|
|
30245
|
+
for (const k of ['x', 'y', 'z'])
|
|
30246
|
+
low[k] = min(low[k], p[k]), high[k] = max(high[k], p[k]);
|
|
30247
|
+
}
|
|
30248
|
+
}
|
|
30249
|
+
// an attached instance is the box around its parts, its handle at the middle of it as an object's
|
|
30250
|
+
// place is the middle of its body, so a game that makes it solid has it solid where it is seen
|
|
30251
|
+
if (children.length)
|
|
30252
|
+
{
|
|
30253
|
+
this.size3D = high.subtract(low);
|
|
30254
|
+
this.originOffset = low.add(high).scale(.5);
|
|
30255
|
+
}
|
|
30256
|
+
for (const part of children)
|
|
30257
|
+
{
|
|
30258
|
+
part.pos3D = part.pos3D.subtract(this.originOffset);
|
|
30259
|
+
this.addChild(part);
|
|
30260
|
+
}
|
|
30261
|
+
}
|
|
30262
|
+
finally
|
|
30263
|
+
{
|
|
30264
|
+
--level3DPrefabDepth;
|
|
30265
|
+
this.attached && --level3DPrefabAttached;
|
|
30266
|
+
}
|
|
30267
|
+
}
|
|
30268
|
+
|
|
30269
|
+
/** Keep the parts with the handle, called automatically each frame */
|
|
30270
|
+
update()
|
|
30271
|
+
{
|
|
30272
|
+
super.update();
|
|
30273
|
+
if (!this.attached && this.partsPlaced !== this.placeKey())
|
|
30274
|
+
this.placeParts();
|
|
30275
|
+
}
|
|
30276
|
+
|
|
30277
|
+
/** Destroy the instance and its parts
|
|
30278
|
+
* @param {boolean} [immediate] */
|
|
30279
|
+
destroy(immediate)
|
|
30280
|
+
{
|
|
30281
|
+
if (this.destroyed) return;
|
|
30282
|
+
for (const part of this.parts)
|
|
30283
|
+
part.parent === this || part.destroy?.(immediate); // children go with their parent
|
|
30284
|
+
super.destroy(immediate);
|
|
30285
|
+
}
|
|
30286
|
+
|
|
30287
|
+
/** A prefab's instance has nothing of its own to draw */
|
|
30288
|
+
render3D() {}
|
|
30289
|
+
}
|
|
30290
|
+
|
|
29666
30291
|
///////////////////////////////////////////////////////////////////////////////
|
|
29667
30292
|
// the built-in types
|
|
29668
30293
|
|
|
@@ -29876,7 +30501,9 @@ function particleEffectSanitize(raw)
|
|
|
29876
30501
|
settings[setting.name] = value === '' || particleEffectShapes.includes(value) ? value : fallback;
|
|
29877
30502
|
else if (isNumber(value))
|
|
29878
30503
|
{
|
|
29879
|
-
|
|
30504
|
+
// an angle past half a turn comes around, a clamp would point it the other way
|
|
30505
|
+
const wraps = setting.name === 'angle' && isFinite(value) && abs(value) > PI;
|
|
30506
|
+
const clamped = clamp(wraps ? mod(value + PI, 2*PI) - PI : value, setting.hardMin, setting.hardMax);
|
|
29880
30507
|
settings[setting.name] = setting.step >= 1 ? round(clamped) : clamped;
|
|
29881
30508
|
}
|
|
29882
30509
|
else
|
|
@@ -29955,9 +30582,11 @@ function particleEffectsGet(name) { return particleEffectsByName.get(String(name
|
|
|
29955
30582
|
* @param {string} text
|
|
29956
30583
|
* @return {Array<Object>}
|
|
29957
30584
|
* @memberof ParticleEffects */
|
|
29958
|
-
function particleEffectsParse(text)
|
|
30585
|
+
function particleEffectsParse(text) { return particleEffectsFromData(JSON.parse(text)); }
|
|
30586
|
+
|
|
30587
|
+
// a library file's data, or one effect's, into sanitized effects
|
|
30588
|
+
function particleEffectsFromData(data)
|
|
29959
30589
|
{
|
|
29960
|
-
const data = JSON.parse(text);
|
|
29961
30590
|
const list = isArray(data?.effects) ? data.effects : data?.settings ? [data] : undefined;
|
|
29962
30591
|
if (!list || !list.length)
|
|
29963
30592
|
throw new Error('no effects in this file');
|
|
@@ -29976,7 +30605,7 @@ function particleEffectsText(effects) { return JSON.stringify({version:1, effect
|
|
|
29976
30605
|
* @memberof ParticleEffects */
|
|
29977
30606
|
async function particleEffectsLoad(url)
|
|
29978
30607
|
{
|
|
29979
|
-
const effects =
|
|
30608
|
+
const effects = particleEffectsFromData(await fetchJSON(url));
|
|
29980
30609
|
particleEffectsAdd(effects);
|
|
29981
30610
|
return effects;
|
|
29982
30611
|
}
|
|
@@ -30154,7 +30783,13 @@ function particleEffectTileInfo(s)
|
|
|
30154
30783
|
* @memberof ParticleEffects */
|
|
30155
30784
|
function particleEffectApply(emitter, effect)
|
|
30156
30785
|
{
|
|
30157
|
-
|
|
30786
|
+
// an effect written by hand or changed since it was added
|
|
30787
|
+
particleEffectSet(emitter, particleEffectSanitize(effect));
|
|
30788
|
+
}
|
|
30789
|
+
|
|
30790
|
+
// set a 2D emitter to an effect that is sanitized already, as a play's is
|
|
30791
|
+
function particleEffectSet(emitter, effect)
|
|
30792
|
+
{
|
|
30158
30793
|
const s = effect.settings, ended = emitter.emitTime < 0; // destroyed, its last particles going
|
|
30159
30794
|
for (const setting of particleEffectSettings)
|
|
30160
30795
|
if (!particleEffectIndirect.includes(setting.name))
|
|
@@ -30190,15 +30825,14 @@ function particleEffectResolve(nameOrEffect, options)
|
|
|
30190
30825
|
* - Attach it to an object with addChild to follow it
|
|
30191
30826
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
30192
30827
|
* @param {Vector2} [pos]
|
|
30193
|
-
* @param {Object} [options]
|
|
30194
|
-
*
|
|
30195
|
-
*
|
|
30196
|
-
*
|
|
30197
|
-
*
|
|
30198
|
-
*
|
|
30199
|
-
* tinted by its colors
|
|
30200
|
-
*
|
|
30201
|
-
* the effect's own for this play
|
|
30828
|
+
* @param {Object} [options] - What to change for this play, each left out when not wanted:
|
|
30829
|
+
* scale grows the whole effect, the built-ins fit a one unit object at 1;
|
|
30830
|
+
* hue turns its colors around the color wheel, 1 is all the way;
|
|
30831
|
+
* saturation multiplies its saturation, 0 is grey;
|
|
30832
|
+
* angle is its direction, 0 is up, the effect's own angle when not given;
|
|
30833
|
+
* tileInfo, a TileInfo or a TextureInfo, is the game's own art to draw with in place of the effect's shape,
|
|
30834
|
+
* tinted by its colors, a whole texture drawn as one tile;
|
|
30835
|
+
* and any effect setting by its name, emitTime, emitRate, speed and the rest, replaces the effect's own
|
|
30202
30836
|
* @return {ParticleEmitter|undefined} - undefined when there is no such effect
|
|
30203
30837
|
* @memberof ParticleEffects */
|
|
30204
30838
|
function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
@@ -30206,7 +30840,7 @@ function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
|
30206
30840
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
30207
30841
|
if (!effect) return;
|
|
30208
30842
|
const emitter = new ParticleEmitter(pos.copy());
|
|
30209
|
-
|
|
30843
|
+
particleEffectSet(emitter, effect);
|
|
30210
30844
|
emitter.scale = options.scale ?? 1;
|
|
30211
30845
|
if (options.angle !== undefined)
|
|
30212
30846
|
emitter.angle = options.angle;
|
|
@@ -30270,7 +30904,7 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
30270
30904
|
p.velocity.y += (p.pos.x - c.x) * s * .002;
|
|
30271
30905
|
};
|
|
30272
30906
|
b('wind').update3D = (p, s)=> p.velocity.x += s * .004 * p.scale * min(p.age / p.lifeTime, 1);
|
|
30273
|
-
// stick has no 3D push:
|
|
30907
|
+
// stick has no 3D push: particleEffectApply3D makes it the emitter's stick, its grip on landing
|
|
30274
30908
|
}
|
|
30275
30909
|
|
|
30276
30910
|
/** Set a 3D emitter to an effect, live, so a running one keeps its particles; its place, scale and flatten stay
|
|
@@ -30279,7 +30913,13 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
30279
30913
|
* @memberof ParticleEffects */
|
|
30280
30914
|
function particleEffectApply3D(emitter, effect)
|
|
30281
30915
|
{
|
|
30282
|
-
|
|
30916
|
+
// an effect written by hand or changed since it was added
|
|
30917
|
+
particleEffectSet3D(emitter, particleEffectSanitize(effect));
|
|
30918
|
+
}
|
|
30919
|
+
|
|
30920
|
+
// set a 3D emitter to an effect that is sanitized already, as a play's is
|
|
30921
|
+
function particleEffectSet3D(emitter, effect)
|
|
30922
|
+
{
|
|
30283
30923
|
const s = effect.settings, e = emitter;
|
|
30284
30924
|
// a circle is a sphere and a rectangle a box as deep as it is wide
|
|
30285
30925
|
e.emitSize = s.emitRect ? vec3(s.emitSize, s.emitHeight, s.emitSize) : s.emitSize;
|
|
@@ -30292,8 +30932,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
30292
30932
|
'randomness', 'additive', 'gravityScale', 'angleSpeed', 'angleDamping', 'collideLevel', 'restitution'])
|
|
30293
30933
|
e[name] = s[name];
|
|
30294
30934
|
e.trailTime = s.trailScale / 60; // a stretch of speed times trailScale is a streak of that many frames
|
|
30295
|
-
|
|
30296
|
-
e.
|
|
30935
|
+
e.friction = s.friction;
|
|
30936
|
+
e.stick = effect.behaviors.find(b=> b.name == 'stick')?.strength ?? 0; // in 3D the emitter grips on landing
|
|
30297
30937
|
// a 2D emitter at angle a shoots along (sin a, cos a), a z turn r takes up to (-sin r, cos r), so r is -a
|
|
30298
30938
|
e.rotation3D = vec3(0, 0, -s.angle);
|
|
30299
30939
|
e.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors, true);
|
|
@@ -30303,8 +30943,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
30303
30943
|
* - The same effect data as particleEffect, so the look carries across: a circle spawn area becomes a sphere and a
|
|
30304
30944
|
* rectangle a box as deep as it is wide, both flat across the way it emits with options.flatten, a disc or a
|
|
30305
30945
|
* sheet; a trail becomes a streak of the same length, and the settings the 3D emitter lacks (particleConeAngle,
|
|
30306
|
-
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the
|
|
30307
|
-
* particle lands
|
|
30946
|
+
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the emitter's
|
|
30947
|
+
* stick, its grip where a particle lands
|
|
30308
30948
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
30309
30949
|
* @param {Vector3} [pos3D]
|
|
30310
30950
|
* @param {Object} [options] - scale, hue, saturation, angle, tileInfo and any setting as particleEffect; angle turns it
|
|
@@ -30316,7 +30956,7 @@ function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
|
|
|
30316
30956
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
30317
30957
|
if (!effect) return;
|
|
30318
30958
|
const e = new ParticleEmitter3D(pos3D.copy());
|
|
30319
|
-
|
|
30959
|
+
particleEffectSet3D(e, effect);
|
|
30320
30960
|
if (options.angle !== undefined)
|
|
30321
30961
|
e.rotation3D = vec3(0, 0, -options.angle);
|
|
30322
30962
|
e.scale3D = vec3(options.scale ?? 1);
|