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.esm.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}
|
|
@@ -3772,6 +3772,15 @@ let touchGamepadEnable = false;
|
|
|
3772
3772
|
* @memberof Settings */
|
|
3773
3773
|
let touchGamepadPassthrough = false;
|
|
3774
3774
|
|
|
3775
|
+
/** True if a pinch of two fingers turns the mouse wheel: touchPinch is added to mouseWheel, so what zooms with
|
|
3776
|
+
* the wheel zooms with a pinch, the mouse is between the two fingers, and a second finger lets go of the button
|
|
3777
|
+
* the first pressed, since a pinch is not a drag; turn it off for a game with its own use for two fingers, the
|
|
3778
|
+
* first finger stays the mouse and touchPinch can still be read
|
|
3779
|
+
* @type {boolean}
|
|
3780
|
+
* @default
|
|
3781
|
+
* @memberof Settings */
|
|
3782
|
+
let touchPinchWheel = true;
|
|
3783
|
+
|
|
3775
3784
|
/** Size of center button if touch gamepad should have start button in the center
|
|
3776
3785
|
* - Prevents activating when pressed near virtual stick or face buttons
|
|
3777
3786
|
* - When the game is paused, any touch will press the button
|
|
@@ -4167,6 +4176,11 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
|
|
|
4167
4176
|
* @memberof Settings */
|
|
4168
4177
|
function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
4169
4178
|
|
|
4179
|
+
/** Set if a pinch of two fingers turns the mouse wheel
|
|
4180
|
+
* @param {boolean} enable
|
|
4181
|
+
* @memberof Settings */
|
|
4182
|
+
function setTouchPinchWheel(enable) { touchPinchWheel = enable; }
|
|
4183
|
+
|
|
4170
4184
|
/** Set if touches outside the gamepad controls should still drive mouse/touch input
|
|
4171
4185
|
* @param {boolean} passthrough
|
|
4172
4186
|
* @memberof Settings */
|
|
@@ -5473,17 +5487,29 @@ class SpriteAnimator
|
|
|
5473
5487
|
* @return {SpriteAnimation} */
|
|
5474
5488
|
get clip() { return this.clips[this.name]; }
|
|
5475
5489
|
|
|
5490
|
+
/** The clip showing now, once a clip that has ended has called its onEnd: the callback may switch clips, and a
|
|
5491
|
+
* read gives the clip it switched to, not the one that ended
|
|
5492
|
+
* @return {SpriteAnimation}
|
|
5493
|
+
* @ignore */
|
|
5494
|
+
get settledClip()
|
|
5495
|
+
{
|
|
5496
|
+
// each clip's own isDone calls its onEnd once; a few switches in a row at most
|
|
5497
|
+
for (let i = 4, clip; i-- && clip !== this.clip;)
|
|
5498
|
+
(clip = this.clip).isDone;
|
|
5499
|
+
return this.clip;
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5476
5502
|
/** The tile of the frame showing now
|
|
5477
5503
|
* @return {TileInfo} */
|
|
5478
|
-
get tileInfo() { return this.
|
|
5504
|
+
get tileInfo() { return this.settledClip.tileInfo; }
|
|
5479
5505
|
|
|
5480
5506
|
/** The frame of the clip showing now
|
|
5481
5507
|
* @return {number} */
|
|
5482
|
-
get frame() { return this.
|
|
5508
|
+
get frame() { return this.settledClip.frame; }
|
|
5483
5509
|
|
|
5484
5510
|
/** True once the clip showing now is a play that has ended
|
|
5485
5511
|
* @return {boolean} */
|
|
5486
|
-
get isDone() { return this.
|
|
5512
|
+
get isDone() { return this.settledClip.isDone; }
|
|
5487
5513
|
}
|
|
5488
5514
|
|
|
5489
5515
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -6922,6 +6948,13 @@ let mouseDeltaScreen = vec2();
|
|
|
6922
6948
|
* @memberof Input */
|
|
6923
6949
|
let mouseWheel = 0;
|
|
6924
6950
|
|
|
6951
|
+
/** Touch pinch delta this frame, in the mouse wheel's units: two fingers moving apart are negative, as the wheel
|
|
6952
|
+
* turned up is, and moving together positive, 1 for every 50 pixels; added to mouseWheel while touchPinchWheel
|
|
6953
|
+
* is on, so a game that zooms with the wheel zooms with a pinch
|
|
6954
|
+
* @type {number}
|
|
6955
|
+
* @memberof Input */
|
|
6956
|
+
let touchPinch = 0;
|
|
6957
|
+
|
|
6925
6958
|
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
6926
6959
|
* @type {boolean}
|
|
6927
6960
|
* @memberof Input */
|
|
@@ -7010,6 +7043,7 @@ function inputClearKey(key, device=0, clearDown=true, clearPressed=true, clearRe
|
|
|
7010
7043
|
const inputWASDToArrow = {KeyW:'ArrowUp', KeyS:'ArrowDown', KeyA:'ArrowLeft', KeyD:'ArrowRight'};
|
|
7011
7044
|
const inputArrowToWASD = {ArrowUp:'KeyW', ArrowDown:'KeyS', ArrowLeft:'KeyA', ArrowRight:'KeyD'};
|
|
7012
7045
|
const inputKeysHeld = new Set; // the keys physically down, since an arrow's slot is shared with its alias
|
|
7046
|
+
let inputPinch; // the two fingers pinching and how far apart they were, undefined with no pinch
|
|
7013
7047
|
let inputWasTouching = 0, inputTouchIdentifier; // the touch driving the mouse, cleared with the input so only a new touch presses
|
|
7014
7048
|
let inputLastTouchTime = -1e9; // when a touch event last came, the mouse events a browser makes from a tap follow it
|
|
7015
7049
|
|
|
@@ -7052,6 +7086,7 @@ function inputClear()
|
|
|
7052
7086
|
if (!i || inputData[i]) inputData[i] = [];
|
|
7053
7087
|
inputKeysHeld.clear();
|
|
7054
7088
|
inputWasTouching = 0;
|
|
7089
|
+
inputPinch = undefined;
|
|
7055
7090
|
// release the touch gamepad, a finger still on it has to lift before it can take a control again
|
|
7056
7091
|
touchGamepadPointerRole.clear();
|
|
7057
7092
|
touchGamepadButtons.length = 0;
|
|
@@ -7579,19 +7614,52 @@ function inputInit()
|
|
|
7579
7614
|
const pressTouch = !inputWasTouching && e.type == 'touchstart' &&
|
|
7580
7615
|
[...e.changedTouches].find(t=> !isGamepadTouch(t));
|
|
7581
7616
|
const touch = pressTouch || gameTouches[0];
|
|
7617
|
+
let pos = vec2(touch.clientX, touch.clientY);
|
|
7618
|
+
|
|
7619
|
+
// two fingers pinch: how far they moved together or apart is the touch screen's wheel, a
|
|
7620
|
+
// click for every 50 pixels, and the mouse is between them, the place to zoom about; with
|
|
7621
|
+
// touchPinchWheel off the pinch is only measured, and the first finger is the mouse as ever
|
|
7622
|
+
const pinching = touching > 1 && touchPinchWheel;
|
|
7623
|
+
if (touching > 1)
|
|
7624
|
+
{
|
|
7625
|
+
const [a, b] = gameTouches, key = a.identifier + ' ' + b.identifier;
|
|
7626
|
+
const distance = hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
7627
|
+
if (inputPinch && inputPinch.key === key)
|
|
7628
|
+
{
|
|
7629
|
+
const delta = (inputPinch.distance - distance) / 50;
|
|
7630
|
+
touchPinch += delta;
|
|
7631
|
+
if (touchPinchWheel)
|
|
7632
|
+
mouseWheel += delta;
|
|
7633
|
+
}
|
|
7634
|
+
inputPinch = {key, distance}; // other fingers start it over, with no jump
|
|
7635
|
+
if (pinching)
|
|
7636
|
+
pos = vec2((a.clientX + b.clientX) / 2, (a.clientY + b.clientY) / 2);
|
|
7637
|
+
}
|
|
7638
|
+
else
|
|
7639
|
+
inputPinch = undefined;
|
|
7582
7640
|
|
|
7583
7641
|
// set event pos and pass it along
|
|
7584
|
-
const pos = vec2(touch.clientX, touch.clientY);
|
|
7585
7642
|
const mousePosScreenLast = mousePosScreen;
|
|
7586
7643
|
mousePosScreen = mouseEventToScreen(pos);
|
|
7587
|
-
if (
|
|
7644
|
+
if (pinching)
|
|
7645
|
+
{
|
|
7646
|
+
// a pinch is not a press or a drag, the finger that pressed lets go
|
|
7647
|
+
if (inputData[0][button] & 1)
|
|
7648
|
+
inputData[0][button] = inputData[0][button] & 2 | 4;
|
|
7649
|
+
}
|
|
7650
|
+
else if (inputWasTouching && touch.identifier === inputTouchIdentifier)
|
|
7588
7651
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
7589
7652
|
else if (pressTouch)
|
|
7590
7653
|
inputData[0][button] = 3;
|
|
7591
|
-
|
|
7654
|
+
// the finger left after a pinch moves the mouse from where it is, with no jump and no press
|
|
7655
|
+
inputTouchIdentifier = pinching ? undefined : touch.identifier;
|
|
7656
|
+
}
|
|
7657
|
+
else
|
|
7658
|
+
{
|
|
7659
|
+
inputPinch = undefined;
|
|
7660
|
+
if (inputWasTouching && (inputData[0][button] & 1))
|
|
7661
|
+
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
7592
7662
|
}
|
|
7593
|
-
else if (inputWasTouching && (inputData[0][button] & 1))
|
|
7594
|
-
inputData[0][button] = inputData[0][button] & 2 | 4; // released only if it was pressed
|
|
7595
7663
|
|
|
7596
7664
|
// set was touching
|
|
7597
7665
|
inputWasTouching = touching;
|
|
@@ -7850,7 +7918,7 @@ function inputUpdatePost()
|
|
|
7850
7918
|
for (const deviceInputData of inputData)
|
|
7851
7919
|
for (const i in deviceInputData)
|
|
7852
7920
|
deviceInputData[i] &= 1;
|
|
7853
|
-
mouseWheel = 0;
|
|
7921
|
+
mouseWheel = touchPinch = 0;
|
|
7854
7922
|
mouseDelta = vec2();
|
|
7855
7923
|
mouseDeltaScreen = vec2();
|
|
7856
7924
|
}
|
|
@@ -8582,10 +8650,9 @@ class Sound
|
|
|
8582
8650
|
ASSERT(isNumber(range), 'range must be a number');
|
|
8583
8651
|
ASSERT(isNumber(taper), 'taper must be a number');
|
|
8584
8652
|
|
|
8585
|
-
/** @property {number} - World space max range of sound */
|
|
8586
8653
|
this.range = range;
|
|
8587
8654
|
/** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
|
|
8588
|
-
* to render3D.soundDefaultRange instead, which is further */
|
|
8655
|
+
* to render3D.soundDefaultRange instead, which is further; setting range makes it false */
|
|
8589
8656
|
this.rangeIsDefault = rangeIsDefault;
|
|
8590
8657
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
8591
8658
|
this.taper = taper;
|
|
@@ -8641,6 +8708,15 @@ class Sound
|
|
|
8641
8708
|
}
|
|
8642
8709
|
}
|
|
8643
8710
|
|
|
8711
|
+
/** World space max range of sound, 0 for no limit; a range that is set is the sound's own, in 3D too
|
|
8712
|
+
* @type {number} */
|
|
8713
|
+
get range() { return this.rangeValue; }
|
|
8714
|
+
set range(range)
|
|
8715
|
+
{
|
|
8716
|
+
this.rangeValue = range;
|
|
8717
|
+
this.rangeIsDefault = false;
|
|
8718
|
+
}
|
|
8719
|
+
|
|
8644
8720
|
/** Sample data for each channel
|
|
8645
8721
|
* Sounds keep their samples in an audio buffer, so reading this rebuilds
|
|
8646
8722
|
* the arrays from it and caches them. The copies are safe to hold onto,
|
|
@@ -14006,13 +14082,13 @@ class LightSystemPlugin
|
|
|
14006
14082
|
// an automatic size follows the canvas, so reallocate the lightmap when
|
|
14007
14083
|
// the canvas changed size, after the flush so the batch keeps its texture
|
|
14008
14084
|
const size = lightSystem.textureSize;
|
|
14009
|
-
|
|
14010
|
-
|
|
14085
|
+
const wantX = glClampTextureSize(mainCanvasSize.x), wantY = glClampTextureSize(mainCanvasSize.y);
|
|
14086
|
+
if (lightSystem.textureSizeAuto && (size.x !== wantX || size.y !== wantY))
|
|
14011
14087
|
{
|
|
14012
|
-
lightSystem.textureSize =
|
|
14088
|
+
lightSystem.textureSize = vec2(wantX, wantY);
|
|
14013
14089
|
glContext.bindTexture(glContext.TEXTURE_2D, lightSystem.texture);
|
|
14014
14090
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA,
|
|
14015
|
-
|
|
14091
|
+
wantX, wantY, 0,
|
|
14016
14092
|
glContext.RGBA, glContext.UNSIGNED_BYTE, null);
|
|
14017
14093
|
// put back the texture the engine tracks, a draw in renderLight must not sample the lightmap
|
|
14018
14094
|
if (glActiveTexture)
|
|
@@ -14152,8 +14228,10 @@ class LightSystemPlugin
|
|
|
14152
14228
|
this.shadowMapSize = glClampTextureSize(this.shadowMapSize);
|
|
14153
14229
|
this.shadowTextureSize = glClampTextureSize(this.shadowTextureSize);
|
|
14154
14230
|
const size = this.textureSize;
|
|
14155
|
-
if (size)
|
|
14156
|
-
|
|
14231
|
+
if (!size) return;
|
|
14232
|
+
const x = glClampTextureSize(size.x), y = glClampTextureSize(size.y);
|
|
14233
|
+
if (x !== size.x || y !== size.y)
|
|
14234
|
+
this.textureSize = vec2(x, y); // only when it is too large, this runs every frame
|
|
14157
14235
|
}
|
|
14158
14236
|
|
|
14159
14237
|
/** Draw a single Light's falloff blob into the currently bound lightmap.
|
|
@@ -20991,6 +21069,179 @@ function sceneUpdate() { paused || !timeScale || sceneCurrent?.update?.(); }
|
|
|
20991
21069
|
function sceneRender() { sceneCurrent?.render?.(); }
|
|
20992
21070
|
function sceneRenderPost() { sceneCurrent?.renderPost?.(); }
|
|
20993
21071
|
|
|
21072
|
+
/**
|
|
21073
|
+
* LittleJS Parallax Plugin
|
|
21074
|
+
* - A background layer that follows the camera by a part of its movement, so it looks far away
|
|
21075
|
+
* - Repeats across the view, so a level of any width has a background
|
|
21076
|
+
* - Draws mountains by default, or anything a game draws into it
|
|
21077
|
+
* @namespace Parallax
|
|
21078
|
+
*/
|
|
21079
|
+
|
|
21080
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
21081
|
+
|
|
21082
|
+
/**
|
|
21083
|
+
* A background layer with parallax: an image that follows the camera by a part of its movement and repeats across
|
|
21084
|
+
* the view, drawn once into a canvas by a function, mountains when none is given
|
|
21085
|
+
* - A far layer has a parallax near 1 and a low renderOrder, a near one a smaller parallax and a higher renderOrder
|
|
21086
|
+
* - The image should meet itself at its left and right edges to repeat without a seam, as the mountains do
|
|
21087
|
+
* @memberof Parallax
|
|
21088
|
+
* @extends CanvasLayer
|
|
21089
|
+
* @example
|
|
21090
|
+
* // three ranges of mountains, the far ones lighter and slower
|
|
21091
|
+
* for (let i = 3; i--;)
|
|
21092
|
+
* new ParallaxLayer(vec2(0, i*2), vec2(40, 20), .9 - i*.2, -1e3 + i,
|
|
21093
|
+
* parallaxMountains(hsl(.6, .3, .7 - i*.2), hsl(.6, .5, .2), i));
|
|
21094
|
+
*
|
|
21095
|
+
* // or draw your own image into the layer
|
|
21096
|
+
* new ParallaxLayer(vec2(), vec2(40, 20), .5, -1e3, (context, size)=>
|
|
21097
|
+
* {
|
|
21098
|
+
* context.fillStyle = '#fff';
|
|
21099
|
+
* context.fillRect(size.x/2 - 20, size.y/2 - 20, 40, 40);
|
|
21100
|
+
* });
|
|
21101
|
+
*/
|
|
21102
|
+
class ParallaxLayer extends CanvasLayer
|
|
21103
|
+
{
|
|
21104
|
+
/** Create a parallax layer and draw its image
|
|
21105
|
+
* @param {Vector2} [pos] - Where the middle of the image is in the world when the camera is there too
|
|
21106
|
+
* @param {Vector2} [size] - World size of one copy of the image
|
|
21107
|
+
* @param {number|Vector2} [parallax] - How much of the camera's movement it follows, 0 stays with the world
|
|
21108
|
+
* and 1 with the screen, a Vector2 to follow x and y by different amounts
|
|
21109
|
+
* @param {number} [renderOrder] - Low to draw behind the game, far layers lowest
|
|
21110
|
+
* @param {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} [drawFunction] - Draws
|
|
21111
|
+
* the image, given the canvas context, its size in pixels and the layer; mountains when not given
|
|
21112
|
+
* @param {Vector2} [canvasSize] - Size of the image in pixels */
|
|
21113
|
+
constructor(pos=vec2(), size=vec2(32, 16), parallax=.5, renderOrder=-1e3, drawFunction=parallaxMountains(),
|
|
21114
|
+
canvasSize=vec2(512, 256))
|
|
21115
|
+
{
|
|
21116
|
+
ASSERT(isNumber(parallax) || isVector2(parallax), 'parallax must be a number or a Vector2');
|
|
21117
|
+
ASSERT(typeof drawFunction == 'function', 'drawFunction must be a function');
|
|
21118
|
+
super(pos, size, 0, renderOrder, canvasSize);
|
|
21119
|
+
|
|
21120
|
+
/** @property {Vector2} - How much of the camera's movement it follows on each axis, 0 stays with the world
|
|
21121
|
+
* and 1 with the screen */
|
|
21122
|
+
this.parallax = isVector2(parallax) ? /** @type {Vector2} */ (parallax).copy() :
|
|
21123
|
+
vec2(/** @type {number} */ (parallax));
|
|
21124
|
+
/** @property {boolean} - Repeat the image across the view, left and right */
|
|
21125
|
+
this.wrapX = true;
|
|
21126
|
+
/** @property {boolean} - Repeat the image up and down the view */
|
|
21127
|
+
this.wrapY = false;
|
|
21128
|
+
/** @property {number} - How much the camera's zoom changes its size on the screen: 1 like the world, 0 not
|
|
21129
|
+
* at all, the same on the screen at any zoom as it is at zoomScale */
|
|
21130
|
+
this.zoomFollow = 1;
|
|
21131
|
+
/** @property {number} - The camera scale its size is given for, used when zoomFollow is under 1 */
|
|
21132
|
+
this.zoomScale = cameraScale;
|
|
21133
|
+
/** @property {function(OffscreenCanvasRenderingContext2D, Vector2, ParallaxLayer): void} - Draws the
|
|
21134
|
+
* image, redraw() after changing it */
|
|
21135
|
+
this.drawFunction = drawFunction;
|
|
21136
|
+
this.redraw();
|
|
21137
|
+
}
|
|
21138
|
+
|
|
21139
|
+
/** Draw the image again with drawFunction, after changing it or what it draws from */
|
|
21140
|
+
redraw()
|
|
21141
|
+
{
|
|
21142
|
+
const context = this.context;
|
|
21143
|
+
if (!context) return; // headless
|
|
21144
|
+
const size = vec2(this.canvas.width, this.canvas.height);
|
|
21145
|
+
context.clearRect(0, 0, size.x, size.y);
|
|
21146
|
+
this.drawFunction(context, size, this);
|
|
21147
|
+
this.updateWebGL();
|
|
21148
|
+
}
|
|
21149
|
+
|
|
21150
|
+
// how much smaller it is drawn than its size, from the camera's zoom and how much of it the layer follows
|
|
21151
|
+
zoomFactor()
|
|
21152
|
+
{ return this.zoomFollow < 1 && cameraScale > 0 ? (this.zoomScale / cameraScale) ** (1 - this.zoomFollow) : 1; }
|
|
21153
|
+
|
|
21154
|
+
/** World size one copy of the image is drawn at now, its size unless zoomFollow is under 1
|
|
21155
|
+
* @return {Vector2} */
|
|
21156
|
+
getDrawSize() { return this.size.scale(this.zoomFactor()); }
|
|
21157
|
+
|
|
21158
|
+
/** Where each copy of the image is drawn now, in the world: one place, or with wrapping every place that
|
|
21159
|
+
* shows in the view
|
|
21160
|
+
* @return {Array<Vector2>} */
|
|
21161
|
+
getDrawPositions()
|
|
21162
|
+
{
|
|
21163
|
+
// its place seen from the camera is cut down by the parallax, and by the zoom it does not follow
|
|
21164
|
+
const zoom = this.zoomFactor(), size = this.size.scale(zoom), p = this.parallax;
|
|
21165
|
+
const offset = this.pos.subtract(cameraPos);
|
|
21166
|
+
const center = cameraPos.add(vec2(offset.x * (1 - p.x) * zoom, offset.y * (1 - p.y) * zoom));
|
|
21167
|
+
|
|
21168
|
+
// the copies that reach into the view; a turned camera sees as far as its corner each way
|
|
21169
|
+
const view = mainCanvasSize.scale(.5 / (cameraScale || 1));
|
|
21170
|
+
const reach = cameraAngle ? vec2(view.length()) : view;
|
|
21171
|
+
const range = (wrap, axis)=>
|
|
21172
|
+
{
|
|
21173
|
+
if (!wrap || !(size[axis] > 0)) return [0, 0];
|
|
21174
|
+
const from = cameraPos[axis] - reach[axis] - size[axis] / 2 - center[axis];
|
|
21175
|
+
const to = cameraPos[axis] + reach[axis] + size[axis] / 2 - center[axis];
|
|
21176
|
+
const first = ceil(from / size[axis]);
|
|
21177
|
+
return [first, min(floor(to / size[axis]), first + 99)]; // a tiny image does not draw without end
|
|
21178
|
+
};
|
|
21179
|
+
const [x0, x1] = range(this.wrapX, 'x'), [y0, y1] = range(this.wrapY, 'y'), positions = [];
|
|
21180
|
+
for (let y = y0; y <= y1; ++y)
|
|
21181
|
+
for (let x = x0; x <= x1; ++x)
|
|
21182
|
+
positions.push(vec2(center.x + x * size.x, center.y + y * size.y));
|
|
21183
|
+
return positions;
|
|
21184
|
+
}
|
|
21185
|
+
|
|
21186
|
+
// Render the layer, called automatically by the engine
|
|
21187
|
+
render()
|
|
21188
|
+
{
|
|
21189
|
+
const size = this.getDrawSize();
|
|
21190
|
+
for (const pos of this.getDrawPositions())
|
|
21191
|
+
this.draw(pos, size, this.color, 0, this.mirror, this.additiveColor, false);
|
|
21192
|
+
}
|
|
21193
|
+
}
|
|
21194
|
+
|
|
21195
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
21196
|
+
|
|
21197
|
+
// the ridge of a range of mountains: a height for each column of an image and one more, the last the same as the
|
|
21198
|
+
// first so the image repeats with no step; it wanders about the middle, turning now and then
|
|
21199
|
+
function parallaxRidge(width, height, random)
|
|
21200
|
+
{
|
|
21201
|
+
const pointiness = .2, levelness = .005, slopeRange = height / 256; // how often it turns, how hard it is
|
|
21202
|
+
const ridge = new Float32Array(width + 1), middle = height / 2; // pulled level, how steep it gets
|
|
21203
|
+
let y = middle, slope = random.float(-slopeRange, slopeRange);
|
|
21204
|
+
for (let x = 0; x <= width; ++x)
|
|
21205
|
+
{
|
|
21206
|
+
ridge[x] = y;
|
|
21207
|
+
y += slope -= (y - middle) * levelness;
|
|
21208
|
+
if (random.float() < pointiness)
|
|
21209
|
+
slope = random.float(-slopeRange, slopeRange);
|
|
21210
|
+
}
|
|
21211
|
+
// take out the difference between its ends, a little from each column, and keep it inside the image
|
|
21212
|
+
const drift = ridge[width] - ridge[0];
|
|
21213
|
+
for (let x = 0; x <= width; ++x)
|
|
21214
|
+
ridge[x] = clamp(ridge[x] - drift * x / width, 0, height);
|
|
21215
|
+
return ridge;
|
|
21216
|
+
}
|
|
21217
|
+
|
|
21218
|
+
/** A draw function for a ParallaxLayer: a range of mountains across the image, shaded from the color of their
|
|
21219
|
+
* peaks down to the color of their feet, that repeats with no seam
|
|
21220
|
+
* @param {Color} [topColor] - Color at the peaks
|
|
21221
|
+
* @param {Color} [bottomColor] - Color at the bottom of the image
|
|
21222
|
+
* @param {number} [seed] - The same seed gives the same mountains, random when not given
|
|
21223
|
+
* @return {function(OffscreenCanvasRenderingContext2D, Vector2): void}
|
|
21224
|
+
* @memberof Parallax */
|
|
21225
|
+
function parallaxMountains(topColor=hsl(.6, .2, .6), bottomColor=hsl(.6, .3, .25), seed)
|
|
21226
|
+
{
|
|
21227
|
+
return (context, size)=>
|
|
21228
|
+
{
|
|
21229
|
+
const w = size.x, h = size.y;
|
|
21230
|
+
const random = new RandomGenerator(seed === undefined ? randInt(1e9) + 1 : seed * 7919 + 1);
|
|
21231
|
+
const ridge = parallaxRidge(w, h, random);
|
|
21232
|
+
|
|
21233
|
+
// a strip one pixel wide with the shading, stretched down each column from its peak
|
|
21234
|
+
const strip = createCanvasContext(1, h);
|
|
21235
|
+
for (let i = h; i--;)
|
|
21236
|
+
{
|
|
21237
|
+
strip.fillStyle = topColor.lerp(bottomColor, i / h).toString();
|
|
21238
|
+
strip.fillRect(0, i, 1, 1);
|
|
21239
|
+
}
|
|
21240
|
+
for (let x = w; x--;)
|
|
21241
|
+
ridge[x] < h && context.drawImage(strip.canvas, 0, 0, 1, h, x, ridge[x], 1, h - ridge[x]);
|
|
21242
|
+
};
|
|
21243
|
+
}
|
|
21244
|
+
|
|
20994
21245
|
/**
|
|
20995
21246
|
* LittleJS PathFinder Plugin
|
|
20996
21247
|
* - Grid-based A* pathfinder with two-pass smoothing for natural-looking paths
|
|
@@ -23257,12 +23508,17 @@ function render3DShadowCaster()
|
|
|
23257
23508
|
light.intensity > 0 && light.color.a > 0 ? light : undefined;
|
|
23258
23509
|
}
|
|
23259
23510
|
|
|
23511
|
+
// the widest cone a shadow map can look down, to each side; the light that casts the shadows lights no wider, or
|
|
23512
|
+
// what is past the map's edge would be lit with nothing to shade it
|
|
23513
|
+
const RENDER3D_SHADOW_CONE_MAX = 1.35;
|
|
23514
|
+
|
|
23260
23515
|
// a light's cone as the four numbers the shader gets: the way it shines, scaled so that its dot with the way to
|
|
23261
23516
|
// 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
|
|
23262
23517
|
// gets numbers that make it 1 every way
|
|
23263
23518
|
function render3DLightCone(light)
|
|
23264
23519
|
{
|
|
23265
|
-
const
|
|
23520
|
+
const widest = light === render3DShadowCaster() ? RENDER3D_SHADOW_CONE_MAX : PI;
|
|
23521
|
+
const angle = light.directional ? 0 : min(light.coneAngle, widest);
|
|
23266
23522
|
if (!(angle > 0)) return [0, 0, 0, -1];
|
|
23267
23523
|
const outer = cos(angle), inner = cos(angle * (1 - clamp(light.coneSoftness)));
|
|
23268
23524
|
const k = 1 / max(inner - outer, 1e-4), forward = light.getForward3D();
|
|
@@ -24049,8 +24305,9 @@ class Render3DPlugin
|
|
|
24049
24305
|
// a spotlight's map looks down its cone with perspective, from just in front of it out to its radius
|
|
24050
24306
|
const pos = caster.getWorldPos3D(), forward = caster.getForward3D();
|
|
24051
24307
|
const up = abs(forward.y) > .99 ? vec3(0, 0, 1) : vec3(0, 1, 0);
|
|
24052
|
-
|
|
24053
|
-
const
|
|
24308
|
+
// the near plane stays in front of the far one however small the light
|
|
24309
|
+
const far = caster.radius, near = min(max(far / 500, .02), far / 2);
|
|
24310
|
+
const fov = min(caster.coneAngle, RENDER3D_SHADOW_CONE_MAX) * 2 + .1;
|
|
24054
24311
|
this.shadowMatrix = Matrix4.perspective(fov, 1, near, far).multiply(
|
|
24055
24312
|
Matrix4.lookAt(pos, pos.add(forward), up).invert());
|
|
24056
24313
|
this.shadowPlanes = render3DFrustumPlanes(this.shadowMatrix);
|
|
@@ -27484,7 +27741,7 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
27484
27741
|
* - intensity multiplies the color, above 1 for a light brighter than white
|
|
27485
27742
|
* - radius is a world distance, so scale3D does not change it
|
|
27486
27743
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
27487
|
-
* - Draws nothing
|
|
27744
|
+
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
27488
27745
|
* @extends EngineObject3D
|
|
27489
27746
|
* @memberof Render3D
|
|
27490
27747
|
* @example
|
|
@@ -27533,8 +27790,10 @@ class Light3D extends EngineObject3D
|
|
|
27533
27790
|
const r = render3D, c = this.color, pos = render3DObjectMatrix(this).getTranslation();
|
|
27534
27791
|
const toCamera = r.camera.pos.subtract(pos), distance = toCamera.length();
|
|
27535
27792
|
const at = distance ? pos.add(toCamera.scale(min(this.glow, distance) / 2 / distance)) : pos;
|
|
27536
|
-
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1));
|
|
27537
|
-
|
|
27793
|
+
const color = rgb(c.r, c.g, c.b, c.a * min(this.intensity, 1)), texture = engineGlowTexture(this.glowFalloff);
|
|
27794
|
+
// a browser that can not make the glow's texture draws no glow, not a plain square
|
|
27795
|
+
if (texture || !glContext)
|
|
27796
|
+
r.drawBillboard(at, vec2(this.glow), texture, color);
|
|
27538
27797
|
}
|
|
27539
27798
|
}
|
|
27540
27799
|
|
|
@@ -27544,7 +27803,7 @@ class Light3D extends EngineObject3D
|
|
|
27544
27803
|
* - It shines from its position toward the origin, like a three.js DirectionalLight: only the direction to it
|
|
27545
27804
|
* counts, so moving it or its parent swings the light around; parent it to a sun in the sky and it follows
|
|
27546
27805
|
* - It cannot sit on the origin, since that leaves no direction
|
|
27547
|
-
* -
|
|
27806
|
+
* - It casts no shadow: the sun, render3D.sunDirection, does, or a spotlight set as render3D.shadowLight
|
|
27548
27807
|
* @extends Light3D
|
|
27549
27808
|
* @memberof Render3D
|
|
27550
27809
|
* @example
|
|
@@ -28621,6 +28880,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28621
28880
|
/** @property {boolean} - Particles hit the level, the height maps and voxel maps, bouncing by restitution and
|
|
28622
28881
|
* sliding along by friction; off by default, it tests each particle's move against the level every frame */
|
|
28623
28882
|
this.collideLevel = false;
|
|
28883
|
+
/** @property {number} - How much a particle grips where it lands, 0 to 1: its speed along the surface is
|
|
28884
|
+
* cut by this much on each hit, on top of the friction, 1 stops it there */
|
|
28885
|
+
this.stick = 0;
|
|
28624
28886
|
/** @property {Particle3DCallback|undefined} - Called with each particle as it is made
|
|
28625
28887
|
* @type {Particle3DCallback|undefined} */
|
|
28626
28888
|
this.particleCreateCallback = undefined;
|
|
@@ -28640,7 +28902,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28640
28902
|
/** @property {Vector3|undefined} - Where the emitter was at its last update, for when its parent is destroyed
|
|
28641
28903
|
* @type {Vector3|undefined} */
|
|
28642
28904
|
this.worldPos3D = undefined;
|
|
28643
|
-
this.emitTimeBuffer =
|
|
28905
|
+
this.emitTimeBuffer = 1; // the first particle comes at once, as the 2D emitter's does
|
|
28644
28906
|
}
|
|
28645
28907
|
|
|
28646
28908
|
/** Spawn new particles, move the live ones, and go away when done */
|
|
@@ -28657,9 +28919,10 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28657
28919
|
{
|
|
28658
28920
|
// a rate of zero is an emitter fed by hand, and the global scale only quiets it,
|
|
28659
28921
|
// neither is a reason to stop counting down the emit time
|
|
28660
|
-
|
|
28922
|
+
const rate = this.emitRate * particleEmitRateScale;
|
|
28923
|
+
if (rate > 0 && rate < Infinity)
|
|
28661
28924
|
{
|
|
28662
|
-
this.emitTimeBuffer +=
|
|
28925
|
+
this.emitTimeBuffer += rate * timeDelta;
|
|
28663
28926
|
for (; this.emitTimeBuffer >= 1; --this.emitTimeBuffer)
|
|
28664
28927
|
this.emitParticle();
|
|
28665
28928
|
}
|
|
@@ -28699,7 +28962,9 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28699
28962
|
const vz = data[k+5] = data[k+5] * damping + gravityZ;
|
|
28700
28963
|
data[k] += vx, data[k+1] += vy, data[k+2] += vz;
|
|
28701
28964
|
data[k+18] += data[k+19] *= angleDamping;
|
|
28702
|
-
|
|
28965
|
+
// true when its collide callback destroyed it, it has had its last callback but the destroy one
|
|
28966
|
+
const gone = collideLevel && this.particleCollide(k, data[k] - vx, data[k+1] - vy, data[k+2] - vz);
|
|
28967
|
+
collideLevel && reread();
|
|
28703
28968
|
const t = i * trailMax * 3;
|
|
28704
28969
|
if (trailMax)
|
|
28705
28970
|
{
|
|
@@ -28711,7 +28976,7 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28711
28976
|
trail[j] = data[k], trail[j+1] = data[k+1], trail[j+2] = data[k+2];
|
|
28712
28977
|
data[k+20] = n + 1;
|
|
28713
28978
|
}
|
|
28714
|
-
updateCallback && (this.particleCall(updateCallback, k), reread());
|
|
28979
|
+
updateCallback && !gone && (this.particleCall(updateCallback, k), reread());
|
|
28715
28980
|
if ((data[k+17] += timeDelta) >= data[k+16])
|
|
28716
28981
|
{
|
|
28717
28982
|
// dead: the last particle takes its slot, trail and all, order does not matter
|
|
@@ -28813,13 +29078,14 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28813
29078
|
out[k] = view.pos.x, out[k+1] = view.pos.y, out[k+2] = view.pos.z;
|
|
28814
29079
|
out[k+3] = view.velocity.x, out[k+4] = view.velocity.y, out[k+5] = view.velocity.z;
|
|
28815
29080
|
if (view.destroyed)
|
|
28816
|
-
out[k+17] = max(out[k+17], out[k+16]
|
|
29081
|
+
out[k+17] = max(out[k+17], out[k+16]); // its life lived: a step short of it can round to just under
|
|
28817
29082
|
return result;
|
|
28818
29083
|
}
|
|
28819
29084
|
|
|
28820
29085
|
// stop the particle at k where its last move went into the level, from where it was: back at the surface, a hair
|
|
28821
29086
|
// off it, its speed into it turned around by restitution and its speed along it kept by friction, unless the
|
|
28822
|
-
// collide callback lets it through; one that starts inside is let go, as a 2D one is
|
|
29087
|
+
// collide callback lets it through; one that starts inside is let go, as a 2D one is; true when the callback
|
|
29088
|
+
// destroyed it
|
|
28823
29089
|
particleCollide(k, x, y, z)
|
|
28824
29090
|
{
|
|
28825
29091
|
let data = this.particleData;
|
|
@@ -28832,18 +29098,21 @@ class ParticleEmitter3D extends EngineObject3D
|
|
|
28832
29098
|
if (h && (!hit || h.distance < hit.distance))
|
|
28833
29099
|
hit = h, level = l;
|
|
28834
29100
|
}
|
|
28835
|
-
if (!hit) return;
|
|
29101
|
+
if (!hit) return false;
|
|
28836
29102
|
const point = from.lerp(to, hit.distance);
|
|
28837
29103
|
const callback = this.particleCollideCallback;
|
|
28838
29104
|
if (callback && (!this.particleCall(callback, k, level, point.copy()) || this.particleView.destroyed))
|
|
28839
|
-
return; // let through, or destroyed by the callback
|
|
29105
|
+
return this.particleView.destroyed; // let through, or destroyed by the callback
|
|
28840
29106
|
data = this.particleData; // as the callback left it, an emit may have grown it
|
|
28841
29107
|
|
|
28842
29108
|
const n = hit.normal, v = vec3(data[k+3], data[k+4], data[k+5]), into = n.scale(v.dot(n));
|
|
28843
|
-
|
|
29109
|
+
// the larger friction of the two, as objects take it, then the emitter's own grip, which a level can not undo
|
|
29110
|
+
const restitution = max(this.restitution, level.restitution);
|
|
29111
|
+
const friction = max(this.friction, level.friction) * (1 - clamp(this.stick));
|
|
28844
29112
|
const out = v.subtract(into).scale(friction).subtract(into.scale(restitution)), p = point.add(n.scale(1e-3));
|
|
28845
29113
|
data[k] = p.x, data[k+1] = p.y, data[k+2] = p.z;
|
|
28846
29114
|
data[k+3] = out.x, data[k+4] = out.y, data[k+5] = out.z;
|
|
29115
|
+
return false;
|
|
28847
29116
|
}
|
|
28848
29117
|
|
|
28849
29118
|
/** Draw the particles, as flat squares or as streaks when trailTime is set
|
|
@@ -29107,6 +29376,11 @@ class LensFlare3D extends EngineObject3D
|
|
|
29107
29376
|
* height; shape is glow, disc or ring
|
|
29108
29377
|
* @type {Array<{at: number, size: number, color: Color, shape: string}>|undefined} */
|
|
29109
29378
|
this.elements = undefined;
|
|
29379
|
+
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
29380
|
+
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
29381
|
+
* in front of the light, and a spotlight's shows from inside its beam only
|
|
29382
|
+
* @type {Light3D|undefined} */
|
|
29383
|
+
this.light = undefined;
|
|
29110
29384
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
29111
29385
|
this.occlusion = true;
|
|
29112
29386
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
@@ -29145,13 +29419,30 @@ class LensFlare3D extends EngineObject3D
|
|
|
29145
29419
|
return this.made;
|
|
29146
29420
|
}
|
|
29147
29421
|
|
|
29148
|
-
|
|
29422
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun, and a point
|
|
29423
|
+
// to find it on the screen by; undefined with no sun direction, or a light that is gone or at the camera
|
|
29424
|
+
flareSource()
|
|
29425
|
+
{
|
|
29426
|
+
const camera = render3D.camera.pos, light = this.light;
|
|
29427
|
+
if (!light)
|
|
29428
|
+
{
|
|
29429
|
+
const sun = render3D.sunDirection;
|
|
29430
|
+
if (!sun.lengthSquared()) return;
|
|
29431
|
+
const direction = sun.normalize();
|
|
29432
|
+
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
29433
|
+
}
|
|
29434
|
+
if (light.destroyed) return;
|
|
29435
|
+
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
29436
|
+
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
29437
|
+
}
|
|
29438
|
+
|
|
29439
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
29440
|
+
* behind the camera
|
|
29149
29441
|
* @return {Vector2|undefined} */
|
|
29150
29442
|
getSunScreenPos()
|
|
29151
29443
|
{
|
|
29152
|
-
const
|
|
29153
|
-
|
|
29154
|
-
return render3D.worldToScreen(render3D.camera.pos.add(direction.normalize(100)));
|
|
29444
|
+
const source = this.flareSource();
|
|
29445
|
+
return source && render3D.worldToScreen(source.pos);
|
|
29155
29446
|
}
|
|
29156
29447
|
|
|
29157
29448
|
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
@@ -29165,7 +29456,19 @@ class LensFlare3D extends EngineObject3D
|
|
|
29165
29456
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
29166
29457
|
const strength = this.visible * clamp((1.3 - off) / .5) * this.intensity;
|
|
29167
29458
|
if (!(strength > 0)) return [];
|
|
29168
|
-
|
|
29459
|
+
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
29460
|
+
const light = this.light, source = light && this.flareSource();
|
|
29461
|
+
if (light)
|
|
29462
|
+
{
|
|
29463
|
+
if (!source) return [];
|
|
29464
|
+
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
29465
|
+
const cone = render3DLightCone(light), d = source.direction;
|
|
29466
|
+
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
29467
|
+
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
29468
|
+
if (!(bright > 0)) return [];
|
|
29469
|
+
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
29470
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
29471
|
+
}
|
|
29169
29472
|
return this.getElements().map((e)=>
|
|
29170
29473
|
{
|
|
29171
29474
|
const c = e.color.multiply(tint);
|
|
@@ -29175,15 +29478,32 @@ class LensFlare3D extends EngineObject3D
|
|
|
29175
29478
|
});
|
|
29176
29479
|
}
|
|
29177
29480
|
|
|
29178
|
-
/** Is something between the camera and the sun: the level, or an object that is not see
|
|
29481
|
+
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
29482
|
+
* through
|
|
29179
29483
|
* @return {boolean} */
|
|
29180
29484
|
isSunHidden()
|
|
29181
29485
|
{
|
|
29182
|
-
const
|
|
29183
|
-
if (!
|
|
29184
|
-
const
|
|
29185
|
-
|
|
29186
|
-
|
|
29486
|
+
const source = this.flareSource();
|
|
29487
|
+
if (!source) return true;
|
|
29488
|
+
const ray = new Ray3D(render3D.camera.pos, source.direction), reach = source.distance;
|
|
29489
|
+
const blockers = engineObjects.filter((o)=> o !== this && o instanceof EngineObject3D && !o.destroyed &&
|
|
29490
|
+
!o.transparent && !o.additive);
|
|
29491
|
+
// a voxel map says block by block what is see through: glass, water and leaves let the sun by, and the
|
|
29492
|
+
// ray goes on to the blocks behind them
|
|
29493
|
+
const maps = /** @type {Array<VoxelMap>} */ (typeof VoxelMap == 'undefined' ? [] :
|
|
29494
|
+
blockers.filter((o)=> o instanceof VoxelMap));
|
|
29495
|
+
for (const map of maps)
|
|
29496
|
+
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
29497
|
+
return true;
|
|
29498
|
+
// an object is hit by its box, and a box the camera is inside, a room, a wide floor or the player's own
|
|
29499
|
+
// body, says nothing of what is in the way: the ray the other way hits it too, and it is left out; so is
|
|
29500
|
+
// one the light is inside, its lamp
|
|
29501
|
+
const backward = ray.direction.scale(-1), back = new Ray3D(ray.origin, backward);
|
|
29502
|
+
const lamp = this.light && [new Ray3D(source.pos, ray.direction), new Ray3D(source.pos, backward)];
|
|
29503
|
+
const hits = (r, o)=> render3DRaycastObject(r, o) !== undefined;
|
|
29504
|
+
const around = (o)=> !(o instanceof HeightMap) && (hits(back, o) || lamp && hits(lamp[0], o) && hits(lamp[1], o));
|
|
29505
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !around(o)));
|
|
29506
|
+
return !!hit && hit.distance < reach;
|
|
29187
29507
|
}
|
|
29188
29508
|
|
|
29189
29509
|
/** Ease visible toward whether the sun shows, called automatically each frame */
|
|
@@ -30313,7 +30633,10 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
30313
30633
|
* returned, see level3DVoxelSetup for its sheet
|
|
30314
30634
|
* - A level may hold a terrain, in a terrain block: pos, its center, size, its size in the world along x and z,
|
|
30315
30635
|
* 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
|
|
30316
|
-
* is made a HeightMap, returned with what else was made
|
|
30636
|
+
* is made a HeightMap, returned with what else was made; paint, when it has it, colors its samples: colors, a
|
|
30637
|
+
* 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
|
|
30638
|
+
* - A level may hold prefabs of its own, in a prefabs block, each by its name as level3DAddPrefab takes it; they
|
|
30639
|
+
* are added before its objects are made, one the game added itself keeps its place
|
|
30317
30640
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
30318
30641
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
30319
30642
|
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and shadows; what the block
|
|
@@ -30326,6 +30649,7 @@ function level3DLoad(level)
|
|
|
30326
30649
|
ASSERT(!!level && typeof level === 'object', 'a level is an object, {} for a new one');
|
|
30327
30650
|
ASSERT(!(level.littlejs3D > LEVEL3D_VERSION), 'the level was made by a newer LittleJS');
|
|
30328
30651
|
editor3DLevelLoaded(level); // debug builds: the 3D editor takes the level, its autosaved edits go in first
|
|
30652
|
+
level3DPrefabsAdd(level.prefabs);
|
|
30329
30653
|
level3DSceneApply(level.scene);
|
|
30330
30654
|
const made = [], map = level3DVoxelMap = level3DVoxelsMake(level.voxels);
|
|
30331
30655
|
map && made.push(map);
|
|
@@ -30436,14 +30760,49 @@ function level3DTerrainShape(terrain)
|
|
|
30436
30760
|
height: isNumber(terrain.height) && terrain.height > 0 ? terrain.height : 1, heights};
|
|
30437
30761
|
}
|
|
30438
30762
|
|
|
30763
|
+
// a hex color of a level as a Color, undefined when it is not one
|
|
30764
|
+
function level3DHexColor(hex)
|
|
30765
|
+
{ return /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex) ? new Color().setHex(hex) : undefined; }
|
|
30766
|
+
|
|
30767
|
+
// the paint of a level's terrain block, for a terrain of so many samples: its colors as they are written and a
|
|
30768
|
+
// color for each sample, row after row, 0 for none and 1 the first of the colors; undefined when it has none or
|
|
30769
|
+
// it does not fit the terrain
|
|
30770
|
+
function level3DTerrainPaint(terrain, samples)
|
|
30771
|
+
{
|
|
30772
|
+
const paint = terrain?.paint, colors = paint?.colors, runs = paint?.cells;
|
|
30773
|
+
if (!isArray(colors) || !isArray(runs) || colors.length > 255 || !colors.every(level3DHexColor)) return;
|
|
30774
|
+
const cells = new Uint8Array(samples);
|
|
30775
|
+
let at = 0;
|
|
30776
|
+
for (let i = 0; i < runs.length; i += 2)
|
|
30777
|
+
{
|
|
30778
|
+
const count = runs[i], color = runs[i + 1];
|
|
30779
|
+
if (!(count > 0 && count % 1 === 0 && color >= 0 && color % 1 === 0) || color > colors.length ||
|
|
30780
|
+
at + count > samples) return;
|
|
30781
|
+
cells.fill(color, at, at += count);
|
|
30782
|
+
}
|
|
30783
|
+
return at === samples ? {colors: colors.slice(), cells} : undefined;
|
|
30784
|
+
}
|
|
30785
|
+
|
|
30786
|
+
// give a terrain's map the colors of its block: its own color over the whole of it, or with paint a color for
|
|
30787
|
+
// each sample, the terrain's own where it is not painted; the caller builds the map again
|
|
30788
|
+
function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain, map.rows * map.columns))
|
|
30789
|
+
{
|
|
30790
|
+
const base = level3DHexColor(terrain.color) || WHITE;
|
|
30791
|
+
map.color = base, map.colors = undefined;
|
|
30792
|
+
if (!paint || !paint.cells.some((v)=> v)) return;
|
|
30793
|
+
const palette = [base, ...paint.colors.map(level3DHexColor)];
|
|
30794
|
+
map.color = WHITE;
|
|
30795
|
+
map.colors = map.heights.map((row, r)=> row.map((v, c)=> palette[paint.cells[r * map.columns + c]] || base));
|
|
30796
|
+
}
|
|
30797
|
+
|
|
30439
30798
|
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
30440
30799
|
function level3DTerrainMake(terrain)
|
|
30441
30800
|
{
|
|
30442
30801
|
const shape = level3DTerrainShape(terrain);
|
|
30443
30802
|
if (!shape || typeof HeightMap == 'undefined') return;
|
|
30444
30803
|
const map = new HeightMap(shape.heights, shape.size, shape.height, undefined, shape.pos);
|
|
30445
|
-
|
|
30446
|
-
|
|
30804
|
+
level3DTerrainSetColors(map, terrain);
|
|
30805
|
+
map.colors && map.rebuild();
|
|
30447
30806
|
return map;
|
|
30448
30807
|
}
|
|
30449
30808
|
|
|
@@ -30516,6 +30875,14 @@ function level3DProperties(type, object)
|
|
|
30516
30875
|
// make one object of a level from the type added for its name, undefined when there is no such type or it made
|
|
30517
30876
|
// nothing
|
|
30518
30877
|
function level3DMake(object)
|
|
30878
|
+
{
|
|
30879
|
+
return level3DMakeAt(object, level3DVector(object.pos, vec3()),
|
|
30880
|
+
level3DVector(object.rotation, vec3()).scale(PI / 180), level3DVector(object.scale, vec3(1)));
|
|
30881
|
+
}
|
|
30882
|
+
|
|
30883
|
+
// make a level object's game object at a place, a rotation in radians and a scale, its own as level3DMake gives
|
|
30884
|
+
// them or the ones a prefab's instance puts it at
|
|
30885
|
+
function level3DMakeAt(object, pos, rotation, scale)
|
|
30519
30886
|
{
|
|
30520
30887
|
const type = level3DTypes.get(object.type);
|
|
30521
30888
|
if (!type)
|
|
@@ -30523,7 +30890,7 @@ function level3DMake(object)
|
|
|
30523
30890
|
debug && console.warn(`level3DLoad: no type added for ${object.type}, skipped`);
|
|
30524
30891
|
return;
|
|
30525
30892
|
}
|
|
30526
|
-
const
|
|
30893
|
+
const properties = level3DProperties(type, object);
|
|
30527
30894
|
const {make} = type, count = engineObjects.length;
|
|
30528
30895
|
let result;
|
|
30529
30896
|
try { result = make.prototype ? new make(pos, properties) : make(pos, properties); }
|
|
@@ -30541,16 +30908,263 @@ function level3DMake(object)
|
|
|
30541
30908
|
if (result instanceof EngineObject3D)
|
|
30542
30909
|
{
|
|
30543
30910
|
level3DBaseScale.set(result, result.scale3D.copy());
|
|
30544
|
-
result.rotation3D =
|
|
30545
|
-
result.scale3D = result.scale3D.multiply(
|
|
30911
|
+
result.rotation3D = rotation.copy();
|
|
30912
|
+
result.scale3D = result.scale3D.multiply(scale);
|
|
30546
30913
|
}
|
|
30547
30914
|
|
|
30548
30915
|
// the type's own properties are set on it, one the type has no default for could be a field of the engine's
|
|
30549
30916
|
for (const key in type.defaults)
|
|
30550
30917
|
result[key] = properties[key];
|
|
30918
|
+
// a prefab's instance makes its parts now that it is where it goes
|
|
30919
|
+
result instanceof Prefab3D && result.placeAt(pos);
|
|
30551
30920
|
return result;
|
|
30552
30921
|
}
|
|
30553
30922
|
|
|
30923
|
+
/** Make one object of a type added with level3DAddType, level3DAddMesh or level3DAddPrefab, from code, with no
|
|
30924
|
+
* level: a prefab's instance, or a plain type
|
|
30925
|
+
* @param {string} type - The type's name
|
|
30926
|
+
* @param {Vector3} [pos3D]
|
|
30927
|
+
* @param {Vector3} [rotation3D] - In radians, as an object has it
|
|
30928
|
+
* @param {Vector3} [scale3D] - Times the scale the type makes it with
|
|
30929
|
+
* @param {Object} [properties] - Over the type's defaults
|
|
30930
|
+
* @return {any} - What the type made, a Prefab3D for a prefab, undefined when there is no such type
|
|
30931
|
+
* @memberof Level3D
|
|
30932
|
+
* @example
|
|
30933
|
+
* level3DAddPrefab('House', await fetchJSON('house.json'));
|
|
30934
|
+
* level3DSpawn('House', vec3(10, 0, 0), vec3(0, PI/2, 0)); */
|
|
30935
|
+
function level3DSpawn(type, pos3D=vec3(), rotation3D=vec3(), scale3D=vec3(1), properties)
|
|
30936
|
+
{
|
|
30937
|
+
ASSERT(isVector3(pos3D) && isVector3(rotation3D) && isVector3(scale3D), 'pos3D, rotation3D and scale3D are vec3');
|
|
30938
|
+
return level3DMakeAt(properties ? {type, properties} : {type}, pos3D.copy(), rotation3D, scale3D);
|
|
30939
|
+
}
|
|
30940
|
+
|
|
30941
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
30942
|
+
// prefabs: a prefab is a small level, a list of objects about its own origin, placed many times
|
|
30943
|
+
|
|
30944
|
+
// the prefabs by name: how an instance is built, its objects, fromLevel, true for one a level's own prefabs
|
|
30945
|
+
// block added, which one the game adds takes the place of, and source, the object it was added from, which the
|
|
30946
|
+
// level editor knows a level that is a prefab by
|
|
30947
|
+
/** @type {Map<string, {attached: boolean, objects: Array<Object>, fromLevel: boolean, source: Object}>} */
|
|
30948
|
+
const level3DPrefabs = new Map;
|
|
30949
|
+
|
|
30950
|
+
// how many prefabs deep an instance being made is, a prefab that holds itself is stopped; and how many of those
|
|
30951
|
+
// are attached: a prefab inside an attached one is attached too, or its parts would be left behind in the world
|
|
30952
|
+
let level3DPrefabDepth = 0, level3DPrefabAttached = 0;
|
|
30953
|
+
|
|
30954
|
+
/** Add a prefab: a small level, its objects placed about its own origin, to place many times under one name
|
|
30955
|
+
* - It is a type from then on: a level's object of that type, the level editor's Place list and level3DSpawn make
|
|
30956
|
+
* an instance, a Prefab3D; an instance is made of the prefab as it is then, so the level editor, where a prefab
|
|
30957
|
+
* changes, makes its instances again
|
|
30958
|
+
* - The prefab is a level as the level editor saves it, {objects: [...]}, so the editor is the prefab editor too;
|
|
30959
|
+
* only its objects are used, and they may be of other prefabs
|
|
30960
|
+
* - With attached true in it the parts are children of the instance and move with it as one body, without
|
|
30961
|
+
* collision of their own, a prefab inside it too; its handle is at the middle of the box around them and its
|
|
30962
|
+
* 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
|
|
30963
|
+
* by hand does
|
|
30964
|
+
* - Adding a name again replaces it
|
|
30965
|
+
* @param {string} name - The type its instances have in a level
|
|
30966
|
+
* @param {Object} prefab - {objects, attached}
|
|
30967
|
+
* @memberof Level3D
|
|
30968
|
+
* @example
|
|
30969
|
+
* level3DAddPrefab('Tower', {objects: [{type: 'Box', pos: [0, 1, 0], scale: [2, 2, 2]},
|
|
30970
|
+
* {type: 'Cylinder', pos: [0, 3, 0]}]});
|
|
30971
|
+
* level3DLoad({objects: [{type: 'Tower', pos: [5, 0, 5]}, {type: 'Tower', pos: [-5, 0, 5], rotation: [0, 45, 0]}]}); */
|
|
30972
|
+
function level3DAddPrefab(name, prefab) { level3DPrefabSet(name, prefab, false); }
|
|
30973
|
+
|
|
30974
|
+
// add a prefab, the game's or a level's own
|
|
30975
|
+
function level3DPrefabSet(name, prefab, fromLevel)
|
|
30976
|
+
{
|
|
30977
|
+
ASSERT(!!prefab && typeof prefab === 'object', 'a prefab is an object, {objects: [...]}');
|
|
30978
|
+
name = String(name);
|
|
30979
|
+
const objects = isArray(prefab?.objects) ? prefab.objects.filter((o)=> o && typeof o === 'object') : [];
|
|
30980
|
+
// a copy, what the game or the editor does to its own afterwards does not change the instances made later
|
|
30981
|
+
level3DPrefabs.set(name, {attached: !!prefab?.attached, objects: JSON.parse(JSON.stringify(objects)), fromLevel,
|
|
30982
|
+
source: prefab});
|
|
30983
|
+
// a class of its own, so the level editor makes its instances again as it does a class's
|
|
30984
|
+
level3DTypes.set(name, {make: class extends Prefab3D { constructor(pos) { super(pos, name); } }, defaults: {},
|
|
30985
|
+
tileInfo: undefined});
|
|
30986
|
+
}
|
|
30987
|
+
|
|
30988
|
+
// add a level's own prefabs, each unless the game added one of that name or it is a plain type's name
|
|
30989
|
+
function level3DPrefabsAdd(prefabs)
|
|
30990
|
+
{
|
|
30991
|
+
if (!prefabs || typeof prefabs !== 'object' || isArray(prefabs)) return;
|
|
30992
|
+
for (const name in prefabs)
|
|
30993
|
+
{
|
|
30994
|
+
const known = level3DPrefabs.get(name);
|
|
30995
|
+
if (known ? !known.fromLevel : level3DTypes.has(name)) continue;
|
|
30996
|
+
prefabs[name] && typeof prefabs[name] === 'object' && level3DPrefabSet(name, prefabs[name], true);
|
|
30997
|
+
}
|
|
30998
|
+
}
|
|
30999
|
+
|
|
31000
|
+
/** Load a prefab from a file the level editor saved and add it
|
|
31001
|
+
* @param {string} name - The type its instances have in a level
|
|
31002
|
+
* @param {string} url
|
|
31003
|
+
* @return {Promise<void>}
|
|
31004
|
+
* @memberof Level3D */
|
|
31005
|
+
async function level3DLoadPrefab(name, url) { level3DAddPrefab(name, await fetchJSON(url)); }
|
|
31006
|
+
|
|
31007
|
+
// where an instance puts one of its prefab's objects: the object's own place, turn and size inside the instance's
|
|
31008
|
+
function level3DPrefabPartTransform(instance, object)
|
|
31009
|
+
{
|
|
31010
|
+
const pos = level3DVector(object.pos, vec3()), scale = level3DVector(object.scale, vec3(1));
|
|
31011
|
+
const rotation = level3DVector(object.rotation, vec3()).scale(PI / 180);
|
|
31012
|
+
// the two turns one after the other; the sizes multiply along each axis, which is exact for an instance sized
|
|
31013
|
+
// evenly or a part that is not turned, and as near as a box can be otherwise
|
|
31014
|
+
const turn = buildMatrix(vec3(), instance.rotation3D).multiply(buildMatrix(vec3(), rotation));
|
|
31015
|
+
return {pos: buildMatrix(instance.pos3D, instance.rotation3D, instance.scale3D).transformPoint(pos),
|
|
31016
|
+
rotation: turn.getRotation(), scale: scale.multiply(instance.scale3D)};
|
|
31017
|
+
}
|
|
31018
|
+
|
|
31019
|
+
/**
|
|
31020
|
+
* An instance of a prefab, what a prefab's type makes: a handle with no shape of its own, and its parts, what the
|
|
31021
|
+
* prefab's objects made
|
|
31022
|
+
* - Parts of a prefab that is not attached are objects of their own in the world: moving, turning or sizing the
|
|
31023
|
+
* handle puts them where it now says with its next update, or call placeParts; destroying it destroys them
|
|
31024
|
+
* - Parts of an attached prefab are its children and move with it as one body
|
|
31025
|
+
* @extends EngineObject3D
|
|
31026
|
+
* @memberof Level3D
|
|
31027
|
+
*/
|
|
31028
|
+
class Prefab3D extends EngineObject3D
|
|
31029
|
+
{
|
|
31030
|
+
/** Create an instance of a prefab, made by its type: place one with a level or level3DSpawn
|
|
31031
|
+
* @param {Vector3} [pos3D]
|
|
31032
|
+
* @param {string} [prefabName] - A prefab added with level3DAddPrefab */
|
|
31033
|
+
constructor(pos3D, prefabName='')
|
|
31034
|
+
{
|
|
31035
|
+
super(pos3D);
|
|
31036
|
+
/** @property {string} - The prefab it is an instance of */
|
|
31037
|
+
this.prefabName = prefabName;
|
|
31038
|
+
/** @property {boolean} - Are its parts its children, moving with it as one body */
|
|
31039
|
+
this.attached = !!level3DPrefabs.get(prefabName)?.attached || level3DPrefabAttached > 0;
|
|
31040
|
+
/** @property {Array<any>} - What the prefab's objects made, in the prefab's order */
|
|
31041
|
+
this.parts = [];
|
|
31042
|
+
/** @property {Vector3} - From the prefab's origin to the handle, in the prefab's own space: nothing for
|
|
31043
|
+
* separate parts, and for an attached prefab the middle of the box around its parts, where its handle
|
|
31044
|
+
* is, as an object's place is the middle of its body */
|
|
31045
|
+
this.originOffset = vec3();
|
|
31046
|
+
this.partObjects = []; // the prefab's object of each part
|
|
31047
|
+
this.partsPlaced = ''; // where the handle was when its parts were last placed, undefined parts not made
|
|
31048
|
+
this.partsMade = false;
|
|
31049
|
+
}
|
|
31050
|
+
|
|
31051
|
+
/** Put the instance with its prefab's origin at a place, turned and sized as the handle is: where a level's
|
|
31052
|
+
* object or level3DSpawn says it goes; the handle of an attached prefab is then at the middle of its body
|
|
31053
|
+
* @param {Vector3} pos3D */
|
|
31054
|
+
placeAt(pos3D)
|
|
31055
|
+
{
|
|
31056
|
+
this.pos3D = pos3D.copy();
|
|
31057
|
+
this.placeParts();
|
|
31058
|
+
const o = this.originOffset;
|
|
31059
|
+
if (o.x || o.y || o.z)
|
|
31060
|
+
this.pos3D = buildMatrix(pos3D, this.rotation3D, this.scale3D).transformPoint(o);
|
|
31061
|
+
}
|
|
31062
|
+
|
|
31063
|
+
/** Put the parts where the handle is now, making them the first time; called by the handle's update when it
|
|
31064
|
+
* has moved, turned or changed size */
|
|
31065
|
+
placeParts()
|
|
31066
|
+
{
|
|
31067
|
+
if (this.destroyed) return;
|
|
31068
|
+
this.partsMade ||= (this.makeParts(), true);
|
|
31069
|
+
this.partsPlaced = this.placeKey();
|
|
31070
|
+
if (this.attached) return; // children follow by themselves
|
|
31071
|
+
this.parts.forEach((part, i)=>
|
|
31072
|
+
{
|
|
31073
|
+
if (!(part instanceof EngineObject3D)) return; // what a function made is where it was made
|
|
31074
|
+
const to = level3DPrefabPartTransform(this, this.partObjects[i]);
|
|
31075
|
+
part.rotation3D = to.rotation;
|
|
31076
|
+
part.scale3D = (level3DBaseScale.get(part) ?? vec3(1)).multiply(to.scale);
|
|
31077
|
+
// a prefab inside this one goes by its origin, which is not where an attached one's handle is
|
|
31078
|
+
part instanceof Prefab3D ? part.placeAt(to.pos) : part.pos3D = to.pos;
|
|
31079
|
+
});
|
|
31080
|
+
}
|
|
31081
|
+
|
|
31082
|
+
// the handle's place, turn and size as one text, to tell when it has changed
|
|
31083
|
+
placeKey()
|
|
31084
|
+
{
|
|
31085
|
+
const p = this.pos3D, r = this.rotation3D, s = this.scale3D;
|
|
31086
|
+
return [p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z].join();
|
|
31087
|
+
}
|
|
31088
|
+
|
|
31089
|
+
// make the parts, each where the instance puts it, or attached at its own place inside the instance
|
|
31090
|
+
makeParts()
|
|
31091
|
+
{
|
|
31092
|
+
const prefab = level3DPrefabs.get(this.prefabName);
|
|
31093
|
+
if (!prefab) return;
|
|
31094
|
+
if (level3DPrefabDepth >= 8)
|
|
31095
|
+
{
|
|
31096
|
+
debug && console.error(`level3DLoad: the prefab ${this.prefabName} holds itself, left out there`);
|
|
31097
|
+
return;
|
|
31098
|
+
}
|
|
31099
|
+
++level3DPrefabDepth;
|
|
31100
|
+
this.attached && ++level3DPrefabAttached;
|
|
31101
|
+
try
|
|
31102
|
+
{
|
|
31103
|
+
const low = vec3(Infinity), high = vec3(-Infinity), children = [];
|
|
31104
|
+
for (const object of prefab.objects)
|
|
31105
|
+
{
|
|
31106
|
+
const local = {pos: level3DVector(object.pos, vec3()), scale: level3DVector(object.scale, vec3(1)),
|
|
31107
|
+
rotation: level3DVector(object.rotation, vec3()).scale(PI / 180)};
|
|
31108
|
+
const at = this.attached ? local : level3DPrefabPartTransform(this, object);
|
|
31109
|
+
const part = level3DMakeAt(object, at.pos, at.rotation, at.scale);
|
|
31110
|
+
if (!part) continue; // level3DMakeAt gives an object or nothing
|
|
31111
|
+
this.parts.push(part);
|
|
31112
|
+
this.partObjects.push(object);
|
|
31113
|
+
if (!this.attached || !(part instanceof EngineObject3D)) continue;
|
|
31114
|
+
// a child rides with its parent and has no collision of its own
|
|
31115
|
+
part.setCollision(false, false, false);
|
|
31116
|
+
children.push(part);
|
|
31117
|
+
// the corners of its box as it is turned; its place is its middle, a prefab inside this one too
|
|
31118
|
+
const box = buildMatrix(part.pos3D, part.rotation3D, part.size3D.multiply(part.scale3D));
|
|
31119
|
+
for (let i = 8; i--;)
|
|
31120
|
+
{
|
|
31121
|
+
const p = box.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
|
|
31122
|
+
for (const k of ['x', 'y', 'z'])
|
|
31123
|
+
low[k] = min(low[k], p[k]), high[k] = max(high[k], p[k]);
|
|
31124
|
+
}
|
|
31125
|
+
}
|
|
31126
|
+
// an attached instance is the box around its parts, its handle at the middle of it as an object's
|
|
31127
|
+
// place is the middle of its body, so a game that makes it solid has it solid where it is seen
|
|
31128
|
+
if (children.length)
|
|
31129
|
+
{
|
|
31130
|
+
this.size3D = high.subtract(low);
|
|
31131
|
+
this.originOffset = low.add(high).scale(.5);
|
|
31132
|
+
}
|
|
31133
|
+
for (const part of children)
|
|
31134
|
+
{
|
|
31135
|
+
part.pos3D = part.pos3D.subtract(this.originOffset);
|
|
31136
|
+
this.addChild(part);
|
|
31137
|
+
}
|
|
31138
|
+
}
|
|
31139
|
+
finally
|
|
31140
|
+
{
|
|
31141
|
+
--level3DPrefabDepth;
|
|
31142
|
+
this.attached && --level3DPrefabAttached;
|
|
31143
|
+
}
|
|
31144
|
+
}
|
|
31145
|
+
|
|
31146
|
+
/** Keep the parts with the handle, called automatically each frame */
|
|
31147
|
+
update()
|
|
31148
|
+
{
|
|
31149
|
+
super.update();
|
|
31150
|
+
if (!this.attached && this.partsPlaced !== this.placeKey())
|
|
31151
|
+
this.placeParts();
|
|
31152
|
+
}
|
|
31153
|
+
|
|
31154
|
+
/** Destroy the instance and its parts
|
|
31155
|
+
* @param {boolean} [immediate] */
|
|
31156
|
+
destroy(immediate)
|
|
31157
|
+
{
|
|
31158
|
+
if (this.destroyed) return;
|
|
31159
|
+
for (const part of this.parts)
|
|
31160
|
+
part.parent === this || part.destroy?.(immediate); // children go with their parent
|
|
31161
|
+
super.destroy(immediate);
|
|
31162
|
+
}
|
|
31163
|
+
|
|
31164
|
+
/** A prefab's instance has nothing of its own to draw */
|
|
31165
|
+
render3D() {}
|
|
31166
|
+
}
|
|
31167
|
+
|
|
30554
31168
|
///////////////////////////////////////////////////////////////////////////////
|
|
30555
31169
|
// the built-in types
|
|
30556
31170
|
|
|
@@ -30764,7 +31378,9 @@ function particleEffectSanitize(raw)
|
|
|
30764
31378
|
settings[setting.name] = value === '' || particleEffectShapes.includes(value) ? value : fallback;
|
|
30765
31379
|
else if (isNumber(value))
|
|
30766
31380
|
{
|
|
30767
|
-
|
|
31381
|
+
// an angle past half a turn comes around, a clamp would point it the other way
|
|
31382
|
+
const wraps = setting.name === 'angle' && isFinite(value) && abs(value) > PI;
|
|
31383
|
+
const clamped = clamp(wraps ? mod(value + PI, 2*PI) - PI : value, setting.hardMin, setting.hardMax);
|
|
30768
31384
|
settings[setting.name] = setting.step >= 1 ? round(clamped) : clamped;
|
|
30769
31385
|
}
|
|
30770
31386
|
else
|
|
@@ -30843,9 +31459,11 @@ function particleEffectsGet(name) { return particleEffectsByName.get(String(name
|
|
|
30843
31459
|
* @param {string} text
|
|
30844
31460
|
* @return {Array<Object>}
|
|
30845
31461
|
* @memberof ParticleEffects */
|
|
30846
|
-
function particleEffectsParse(text)
|
|
31462
|
+
function particleEffectsParse(text) { return particleEffectsFromData(JSON.parse(text)); }
|
|
31463
|
+
|
|
31464
|
+
// a library file's data, or one effect's, into sanitized effects
|
|
31465
|
+
function particleEffectsFromData(data)
|
|
30847
31466
|
{
|
|
30848
|
-
const data = JSON.parse(text);
|
|
30849
31467
|
const list = isArray(data?.effects) ? data.effects : data?.settings ? [data] : undefined;
|
|
30850
31468
|
if (!list || !list.length)
|
|
30851
31469
|
throw new Error('no effects in this file');
|
|
@@ -30864,7 +31482,7 @@ function particleEffectsText(effects) { return JSON.stringify({version:1, effect
|
|
|
30864
31482
|
* @memberof ParticleEffects */
|
|
30865
31483
|
async function particleEffectsLoad(url)
|
|
30866
31484
|
{
|
|
30867
|
-
const effects =
|
|
31485
|
+
const effects = particleEffectsFromData(await fetchJSON(url));
|
|
30868
31486
|
particleEffectsAdd(effects);
|
|
30869
31487
|
return effects;
|
|
30870
31488
|
}
|
|
@@ -31042,7 +31660,13 @@ function particleEffectTileInfo(s)
|
|
|
31042
31660
|
* @memberof ParticleEffects */
|
|
31043
31661
|
function particleEffectApply(emitter, effect)
|
|
31044
31662
|
{
|
|
31045
|
-
|
|
31663
|
+
// an effect written by hand or changed since it was added
|
|
31664
|
+
particleEffectSet(emitter, particleEffectSanitize(effect));
|
|
31665
|
+
}
|
|
31666
|
+
|
|
31667
|
+
// set a 2D emitter to an effect that is sanitized already, as a play's is
|
|
31668
|
+
function particleEffectSet(emitter, effect)
|
|
31669
|
+
{
|
|
31046
31670
|
const s = effect.settings, ended = emitter.emitTime < 0; // destroyed, its last particles going
|
|
31047
31671
|
for (const setting of particleEffectSettings)
|
|
31048
31672
|
if (!particleEffectIndirect.includes(setting.name))
|
|
@@ -31078,15 +31702,14 @@ function particleEffectResolve(nameOrEffect, options)
|
|
|
31078
31702
|
* - Attach it to an object with addChild to follow it
|
|
31079
31703
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
31080
31704
|
* @param {Vector2} [pos]
|
|
31081
|
-
* @param {Object} [options]
|
|
31082
|
-
*
|
|
31083
|
-
*
|
|
31084
|
-
*
|
|
31085
|
-
*
|
|
31086
|
-
*
|
|
31087
|
-
* tinted by its colors
|
|
31088
|
-
*
|
|
31089
|
-
* the effect's own for this play
|
|
31705
|
+
* @param {Object} [options] - What to change for this play, each left out when not wanted:
|
|
31706
|
+
* scale grows the whole effect, the built-ins fit a one unit object at 1;
|
|
31707
|
+
* hue turns its colors around the color wheel, 1 is all the way;
|
|
31708
|
+
* saturation multiplies its saturation, 0 is grey;
|
|
31709
|
+
* angle is its direction, 0 is up, the effect's own angle when not given;
|
|
31710
|
+
* tileInfo, a TileInfo or a TextureInfo, is the game's own art to draw with in place of the effect's shape,
|
|
31711
|
+
* tinted by its colors, a whole texture drawn as one tile;
|
|
31712
|
+
* and any effect setting by its name, emitTime, emitRate, speed and the rest, replaces the effect's own
|
|
31090
31713
|
* @return {ParticleEmitter|undefined} - undefined when there is no such effect
|
|
31091
31714
|
* @memberof ParticleEffects */
|
|
31092
31715
|
function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
@@ -31094,7 +31717,7 @@ function particleEffect(nameOrEffect, pos=vec2(), options={})
|
|
|
31094
31717
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
31095
31718
|
if (!effect) return;
|
|
31096
31719
|
const emitter = new ParticleEmitter(pos.copy());
|
|
31097
|
-
|
|
31720
|
+
particleEffectSet(emitter, effect);
|
|
31098
31721
|
emitter.scale = options.scale ?? 1;
|
|
31099
31722
|
if (options.angle !== undefined)
|
|
31100
31723
|
emitter.angle = options.angle;
|
|
@@ -31158,7 +31781,7 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
31158
31781
|
p.velocity.y += (p.pos.x - c.x) * s * .002;
|
|
31159
31782
|
};
|
|
31160
31783
|
b('wind').update3D = (p, s)=> p.velocity.x += s * .004 * p.scale * min(p.age / p.lifeTime, 1);
|
|
31161
|
-
// stick has no 3D push:
|
|
31784
|
+
// stick has no 3D push: particleEffectApply3D makes it the emitter's stick, its grip on landing
|
|
31162
31785
|
}
|
|
31163
31786
|
|
|
31164
31787
|
/** Set a 3D emitter to an effect, live, so a running one keeps its particles; its place, scale and flatten stay
|
|
@@ -31167,7 +31790,13 @@ function particleEffectFromEmitter(emitter, name='Effect')
|
|
|
31167
31790
|
* @memberof ParticleEffects */
|
|
31168
31791
|
function particleEffectApply3D(emitter, effect)
|
|
31169
31792
|
{
|
|
31170
|
-
|
|
31793
|
+
// an effect written by hand or changed since it was added
|
|
31794
|
+
particleEffectSet3D(emitter, particleEffectSanitize(effect));
|
|
31795
|
+
}
|
|
31796
|
+
|
|
31797
|
+
// set a 3D emitter to an effect that is sanitized already, as a play's is
|
|
31798
|
+
function particleEffectSet3D(emitter, effect)
|
|
31799
|
+
{
|
|
31171
31800
|
const s = effect.settings, e = emitter;
|
|
31172
31801
|
// a circle is a sphere and a rectangle a box as deep as it is wide
|
|
31173
31802
|
e.emitSize = s.emitRect ? vec3(s.emitSize, s.emitHeight, s.emitSize) : s.emitSize;
|
|
@@ -31180,8 +31809,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
31180
31809
|
'randomness', 'additive', 'gravityScale', 'angleSpeed', 'angleDamping', 'collideLevel', 'restitution'])
|
|
31181
31810
|
e[name] = s[name];
|
|
31182
31811
|
e.trailTime = s.trailScale / 60; // a stretch of speed times trailScale is a streak of that many frames
|
|
31183
|
-
|
|
31184
|
-
e.
|
|
31812
|
+
e.friction = s.friction;
|
|
31813
|
+
e.stick = effect.behaviors.find(b=> b.name == 'stick')?.strength ?? 0; // in 3D the emitter grips on landing
|
|
31185
31814
|
// 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
|
|
31186
31815
|
e.rotation3D = vec3(0, 0, -s.angle);
|
|
31187
31816
|
e.particleUpdateCallback = particleEffectUpdateCallback(effect.behaviors, true);
|
|
@@ -31191,8 +31820,8 @@ function particleEffectApply3D(emitter, effect)
|
|
|
31191
31820
|
* - The same effect data as particleEffect, so the look carries across: a circle spawn area becomes a sphere and a
|
|
31192
31821
|
* rectangle a box as deep as it is wide, both flat across the way it emits with options.flatten, a disc or a
|
|
31193
31822
|
* sheet; a trail becomes a streak of the same length, and the settings the 3D emitter lacks (particleConeAngle,
|
|
31194
|
-
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the
|
|
31195
|
-
* particle lands
|
|
31823
|
+
* randomColorLinear, velocityInheritance, localSpace) are left out; the stick behavior becomes the emitter's
|
|
31824
|
+
* stick, its grip where a particle lands
|
|
31196
31825
|
* @param {string|Object} nameOrEffect - A built-in or added effect's name, or an effect
|
|
31197
31826
|
* @param {Vector3} [pos3D]
|
|
31198
31827
|
* @param {Object} [options] - scale, hue, saturation, angle, tileInfo and any setting as particleEffect; angle turns it
|
|
@@ -31204,7 +31833,7 @@ function particleEffect3D(nameOrEffect, pos3D=vec3(), options={})
|
|
|
31204
31833
|
const effect = particleEffectResolve(nameOrEffect, options);
|
|
31205
31834
|
if (!effect) return;
|
|
31206
31835
|
const e = new ParticleEmitter3D(pos3D.copy());
|
|
31207
|
-
|
|
31836
|
+
particleEffectSet3D(e, effect);
|
|
31208
31837
|
if (options.angle !== undefined)
|
|
31209
31838
|
e.rotation3D = vec3(0, 0, -options.angle);
|
|
31210
31839
|
e.scale3D = vec3(options.scale ?? 1);
|
|
@@ -32853,32 +33482,101 @@ function debugPoint3D(pos, color=WHITE, time=0, size=.2)
|
|
|
32853
33482
|
* @memberof Editor
|
|
32854
33483
|
*/
|
|
32855
33484
|
|
|
33485
|
+
/**
|
|
33486
|
+
* @typedef {Object} EditorToolAt - What the level editor gives the callbacks of a tool of the game's own
|
|
33487
|
+
* @property {any} pos - Where the mouse is in the level: a Vector2 in the 2D editor; in the 3D one a Vector3 on
|
|
33488
|
+
* the level or the ground, undefined when the mouse is over the panel
|
|
33489
|
+
* @property {Vector2|undefined} cell - 2D: the tile cell under the mouse on the selected tile layer
|
|
33490
|
+
* @property {any} ray - 3D: the mouse's Ray3D
|
|
33491
|
+
* @property {boolean} shift - Is Shift held
|
|
33492
|
+
* @property {boolean} ctrl - Is Ctrl held
|
|
33493
|
+
* @memberof Editor
|
|
33494
|
+
*/
|
|
33495
|
+
|
|
33496
|
+
/**
|
|
33497
|
+
* @typedef {Object} EditorTool - A tool of the game's own for the level editor, see LevelEditor.addTool
|
|
33498
|
+
* @property {string} [key] - The key that picks it, as addKey spells one
|
|
33499
|
+
* @property {string} [hint] - The hint line while it is on
|
|
33500
|
+
* @property {function(EditorToolAt): any} [onPress] - The left button went down in the level; returning false
|
|
33501
|
+
* says the press was not the tool's
|
|
33502
|
+
* @property {function(EditorToolAt): any} [onDrag] - Each frame it is held
|
|
33503
|
+
* @property {function(EditorToolAt): any} [onRelease] - It was let go
|
|
33504
|
+
* @property {function(EditorToolAt): any} [onDraw] - Each frame the tool is on, to draw its cursor or preview
|
|
33505
|
+
* @memberof Editor
|
|
33506
|
+
*/
|
|
33507
|
+
|
|
33508
|
+
/**
|
|
33509
|
+
* @typedef {Object} EditorEdit2D - The 2D level editor's edit functions, levelEditor.edit2D
|
|
33510
|
+
* @property {Object|undefined} map - The Tiled map of the selected layer
|
|
33511
|
+
* @property {TileLayer|undefined} layer - The selected tile layer, undefined on an object layer
|
|
33512
|
+
* @property {Array<Object>} objects - A copy of the selected object layer's objects, as Tiled has them
|
|
33513
|
+
* @property {Vector2|undefined} hover - The cell under the mouse on the selected tile layer
|
|
33514
|
+
* @property {Set<number>} selection - The selected objects' ids
|
|
33515
|
+
* @property {function(Vector2, number, ...any): boolean} paint - Set a cell of the selected tile layer
|
|
33516
|
+
* to a tile index, -1 erases, with a direction 0 to 3 and a mirror; false off the layer or with none
|
|
33517
|
+
* @property {function(function(Array<Object>): void): boolean} changeObjects - Edit a copy of the selected object
|
|
33518
|
+
* layer's objects; false when nothing changed
|
|
33519
|
+
* @property {function(): void} strokeEnd - End the edit: one undo step, and the autosave
|
|
33520
|
+
* @property {function(): void} strokeCancel - Take the edit being made back
|
|
33521
|
+
* @property {function(function(): void): void} bulk - Many paints with one redraw a layer
|
|
33522
|
+
* @property {function(...any): any} undo - Undo, or redo with true
|
|
33523
|
+
* @property {function(): string} toJSON - The map as the text Save writes
|
|
33524
|
+
* @memberof Editor
|
|
33525
|
+
*/
|
|
33526
|
+
|
|
33527
|
+
/**
|
|
33528
|
+
* @typedef {Object} EditorEdit3D - The 3D level editor's edit functions, levelEditor.edit3D; a position, a
|
|
33529
|
+
* rotation and a scale are Vector3, the rotation in degrees
|
|
33530
|
+
* @property {Object|undefined} level - The level object being edited
|
|
33531
|
+
* @property {Array<Object>} objects - Its objects, the list in the level: read it, edit through change
|
|
33532
|
+
* @property {Set<number>} selection - The selected objects' ids
|
|
33533
|
+
* @property {function(): Array<Object>} selected - The selected objects
|
|
33534
|
+
* @property {function(number): any} made - What the game made for an object's id
|
|
33535
|
+
* @property {function(function(Array<Object>): void): boolean} change - Edit a copy of the object list; false
|
|
33536
|
+
* when nothing changed
|
|
33537
|
+
* @property {function(string, function(any): any): boolean} changePart - Edit the level's scene, voxels, terrain
|
|
33538
|
+
* or prefabs block
|
|
33539
|
+
* @property {function(): void} strokeEnd - End the edit: one undo step, and the autosave
|
|
33540
|
+
* @property {function(): void} strokeCancel - Take the edit being made back
|
|
33541
|
+
* @property {function(string, any): number|undefined} place - Add an object of a type at a position, selected;
|
|
33542
|
+
* its id, undefined when there is no such type or the level can not be edited
|
|
33543
|
+
* @property {function(Object, ...any): void} setTransform - Write a place, a rotation and a scale on an
|
|
33544
|
+
* object of the list given to change; one left out is kept
|
|
33545
|
+
* @property {function(Object, string, any): void} setProperty - Write a property on an object of the list given
|
|
33546
|
+
* to change, left out of the file when it is the type's default
|
|
33547
|
+
* @property {function(Object): any} pos - An object's position
|
|
33548
|
+
* @property {function(Object): any} rotation - Its rotation, in degrees
|
|
33549
|
+
* @property {function(Object): any} scale - Its scale
|
|
33550
|
+
* @property {function(): any} mousePoint - Where the mouse is on the level or the ground, undefined over the panel
|
|
33551
|
+
* @property {function(...any): boolean} undo - Undo, or redo with true
|
|
33552
|
+
* @property {function(): string} toJSON - The level as the text Save writes
|
|
33553
|
+
* @memberof Editor
|
|
33554
|
+
*/
|
|
33555
|
+
|
|
32856
33556
|
/**
|
|
32857
33557
|
* The level editor, open it to pause the game and edit its level, close it to play on with the changes
|
|
32858
33558
|
* - One of it, levelEditor, 0 on the debug overlay opens and closes it too
|
|
32859
|
-
* -
|
|
33559
|
+
* - A game makes it its own: it sets the hooks, adds keys, panel buttons and tools, or extends this class and
|
|
33560
|
+
* gives its own with setLevelEditor; the same calls work in the 2D editor and the 3D one, see EDITOR.md
|
|
33561
|
+
* - In release builds LevelEditor is a stub that never opens: what a game adds is taken and never called
|
|
32860
33562
|
* @memberof Editor
|
|
32861
33563
|
* @example
|
|
32862
33564
|
* levelEditor.onRestart = ()=> loadLevel(); // adds a Restart button that rebuilds the level
|
|
32863
33565
|
* levelEditor.onTile = (layer, pos, tile)=> layer.setCollisionData(pos, tile === ladderTile ? -1 : tile ? 1 : 0);
|
|
33566
|
+
* levelEditor.addKey('k', ()=> clearEnemies(), 'K: clear the enemies');
|
|
33567
|
+
*
|
|
33568
|
+
* // or as a class of the game's own
|
|
33569
|
+
* class MyEditor extends LevelEditor
|
|
33570
|
+
* {
|
|
33571
|
+
* onRestart() { loadLevel(); }
|
|
33572
|
+
* onDraw() { drawSpawnZones(); }
|
|
33573
|
+
* }
|
|
33574
|
+
* setLevelEditor(new MyEditor);
|
|
32864
33575
|
*/
|
|
32865
33576
|
class LevelEditor
|
|
32866
33577
|
{
|
|
32867
33578
|
constructor()
|
|
32868
33579
|
{
|
|
32869
|
-
/** @property {EditorTileCallback|undefined} - What the game does when the editor paints a tile, like
|
|
32870
|
-
* setting its collision or its look the way the game does when it loads the level; without one, the
|
|
32871
|
-
* collision layer gets collision 1 where there is a tile
|
|
32872
|
-
* @type {EditorTileCallback|undefined} */
|
|
32873
|
-
this.onTile = undefined;
|
|
32874
|
-
/** @property {Function|undefined} - Rebuild the level from the map the editor changed, a Restart button
|
|
32875
|
-
* calls it after switching to play; without one there is no Restart button
|
|
32876
|
-
* @type {EditorRestartCallback|undefined} */
|
|
32877
|
-
this.onRestart = undefined;
|
|
32878
|
-
/** @property {Function|undefined} - Put the player at a world position; with it, the editor's Advanced
|
|
32879
|
-
* section has Play from mouse, which starts play there, Escape at the mouse and Play at the view center
|
|
32880
|
-
* @type {EditorPlayFromCallback|undefined} */
|
|
32881
|
-
this.onPlayFrom = undefined;
|
|
32882
33580
|
/** @property {Array<number>|undefined} - The tiles the palette shows, in its order, for a sheet that also holds
|
|
32883
33581
|
* sprites and art that are not level tiles; undefined shows every tile of the sheet
|
|
32884
33582
|
* @type {Array<number>|undefined} */
|
|
@@ -32887,15 +33585,133 @@ class LevelEditor
|
|
|
32887
33585
|
* 3D one when a level was loaded with level3DLoad and there is a Render3DPlugin
|
|
32888
33586
|
* @type {boolean|undefined} */
|
|
32889
33587
|
this.use3D = undefined;
|
|
32890
|
-
|
|
33588
|
+
/** @property {string|undefined} - The tool of the game's own that is on, one added with addTool, undefined
|
|
33589
|
+
* while a tool of the editor's is
|
|
33590
|
+
* @type {string|undefined} */
|
|
33591
|
+
this.tool = undefined;
|
|
33592
|
+
// what the game added: its keys by how they are spelled, its panel buttons and its tools by name
|
|
33593
|
+
/** @type {Object<string, {action: function(boolean): any, help: string|undefined, warned: boolean}>} */
|
|
33594
|
+
this.keys = {};
|
|
33595
|
+
/** @type {Array<{label: string, onClick: Function, title: string}>} */
|
|
33596
|
+
this.buttons = [];
|
|
33597
|
+
/** @type {Object<string, EditorTool>} */
|
|
33598
|
+
this.tools = {};
|
|
33599
|
+
}
|
|
33600
|
+
|
|
33601
|
+
/** What the game does when the 2D editor paints a tile, like setting its collision or its look the way the
|
|
33602
|
+
* game does when it loads the level; set it or override it, without one the collision layer gets collision 1
|
|
33603
|
+
* where there is a tile
|
|
33604
|
+
* @param {TileLayer} layer - The layer painted
|
|
33605
|
+
* @param {Vector2} pos - The cell's position in the layer
|
|
33606
|
+
* @param {number|undefined} tile - The tile painted, undefined when erased */
|
|
33607
|
+
onTile(layer, pos, tile) {}
|
|
33608
|
+
|
|
33609
|
+
/** Rebuild the level from what the editor changed; set it or override it and the editor has a Restart button,
|
|
33610
|
+
* which calls it after switching to play */
|
|
33611
|
+
onRestart() {}
|
|
33612
|
+
|
|
33613
|
+
/** Put the player at a position, a Vector2 in the 2D editor and a Vector3 in the 3D one; set it or override it
|
|
33614
|
+
* and the editor has Play from mouse, which starts play there
|
|
33615
|
+
* @param {any} pos */
|
|
33616
|
+
onPlayFrom(pos) {}
|
|
33617
|
+
|
|
33618
|
+
/** Called when the editor opens, to set or override */
|
|
33619
|
+
onOpen() {}
|
|
33620
|
+
|
|
33621
|
+
/** Called when the editor closes and the game plays on, to set or override */
|
|
33622
|
+
onClose() {}
|
|
33623
|
+
|
|
33624
|
+
/** Called each frame while the editor is open, to set or override */
|
|
33625
|
+
onUpdate() {}
|
|
33626
|
+
|
|
33627
|
+
/** Called while the editor draws the level, to draw overlays of the game's own in it, to set or override */
|
|
33628
|
+
onDraw() {}
|
|
33629
|
+
|
|
33630
|
+
/** Called once when the editor's panel is made, with a box in it for the game's own controls, to set or override
|
|
33631
|
+
* @param {HTMLElement} element */
|
|
33632
|
+
onPanel(element) {}
|
|
33633
|
+
|
|
33634
|
+
/** Called by Save with the text of the file and its name; return true when the game kept it itself, and the
|
|
33635
|
+
* editor writes no file, to set or override
|
|
33636
|
+
* @param {string} text
|
|
33637
|
+
* @param {string} fileName
|
|
33638
|
+
* @return {boolean|void} */
|
|
33639
|
+
onSave(text, fileName) { return false; }
|
|
32891
33640
|
|
|
32892
33641
|
/** True while the editor is open, the game is paused under it
|
|
32893
33642
|
* @return {boolean} */
|
|
32894
|
-
get isOpen() { return editorIsOpen; }
|
|
33643
|
+
get isOpen() { return editorIsOpen || !!editorOther?.isOpen(); }
|
|
33644
|
+
|
|
33645
|
+
/** True when the editor in use is the 3D one, as use3D says or since a 3D level was loaded
|
|
33646
|
+
* @return {boolean} */
|
|
33647
|
+
get is3D() { return !!editorOther?.wanted(); }
|
|
33648
|
+
|
|
33649
|
+
/** The 2D editor's edit functions, for a key, a button or a tool of the game's own: map, the Tiled map; layer,
|
|
33650
|
+
* the selected TileLayer; objects, a copy of the selected object layer's; hover, the cell under the mouse;
|
|
33651
|
+
* selection, the selected objects' ids; paint(cell, tile, direction, mirror), a tile index, -1 erases;
|
|
33652
|
+
* changeObjects((list)=> ...); strokeEnd() and strokeCancel(); bulk(()=> ...); undo(redo); toJSON()
|
|
33653
|
+
* @return {EditorEdit2D} */
|
|
33654
|
+
get edit2D() { return editorEdit2D; }
|
|
33655
|
+
|
|
33656
|
+
/** The 3D editor's edit functions, undefined without the 3D plugins: level; objects; selection, a Set of ids;
|
|
33657
|
+
* selected(); made(id); change((list)=> ...); changePart(name, (part)=> ...); strokeEnd() and strokeCancel();
|
|
33658
|
+
* place(type, pos3D); setTransform(object, pos3D, rotationDegrees, scale3D); setProperty(object, name, value);
|
|
33659
|
+
* pos(object), rotation(object) and scale(object); mousePoint(); undo(redo); toJSON()
|
|
33660
|
+
* @return {EditorEdit3D|undefined} */
|
|
33661
|
+
get edit3D() { return editorOther?.edit; }
|
|
33662
|
+
|
|
33663
|
+
/** Add a key of the game's own to the editor, the 2D and the 3D one
|
|
33664
|
+
* - Escape and 0 are the editor's own, to play and to exit, and Ctrl is the one modifier: Shift is given to
|
|
33665
|
+
* the action
|
|
33666
|
+
* @param {string} key - A letter or digit, 'k', or a key's name in any case, 'Delete', 'F2', 'Enter', 'Space';
|
|
33667
|
+
* 'ctrl+k' with Ctrl or Cmd
|
|
33668
|
+
* @param {function(boolean): any} action - Called with whether Shift is held; returning false says it did
|
|
33669
|
+
* nothing, and the key is left to the browser
|
|
33670
|
+
* @param {string} [helpLine] - A line for the editor's help */
|
|
33671
|
+
addKey(key, action, helpLine)
|
|
33672
|
+
{
|
|
33673
|
+
ASSERT(typeof action == 'function', 'a key needs an action');
|
|
33674
|
+
const spelled = editorKeySpelling(key), name = spelled.replace('ctrl+', '');
|
|
33675
|
+
if (name === 'Escape' || name === '0' || name.length > 1 && name.includes('+'))
|
|
33676
|
+
{
|
|
33677
|
+
console.warn(`levelEditor.addKey: ${key} is not taken, ` + (name.includes('+') ?
|
|
33678
|
+
'Ctrl is the one modifier, Shift is given to the action' : 'Escape and 0 are the editor\'s own'));
|
|
33679
|
+
return;
|
|
33680
|
+
}
|
|
33681
|
+
this.keys[spelled] = {action, help: helpLine, warned: false};
|
|
33682
|
+
this === levelEditor && editorKeyClashes();
|
|
33683
|
+
}
|
|
33684
|
+
|
|
33685
|
+
/** Add a button of the game's own to the editor's panel
|
|
33686
|
+
* @param {string} label
|
|
33687
|
+
* @param {Function} onClick
|
|
33688
|
+
* @param {string} [title] - What it says when the mouse is over it */
|
|
33689
|
+
addButton(label, onClick, title='') { this.buttons.push({label, onClick, title}); }
|
|
33690
|
+
|
|
33691
|
+
/** Add a tool of the game's own: a button in the panel by its name, and while it is on the left button in the
|
|
33692
|
+
* level is its, not the editor's own tools'
|
|
33693
|
+
* - Each callback is given {pos, cell, ray, shift, ctrl}: pos is where the mouse is in the level, a Vector2 in
|
|
33694
|
+
* 2D and a Vector3 on the level or the ground in 3D; cell is the tile under it in 2D; ray is the mouse's
|
|
33695
|
+
* in 3D
|
|
33696
|
+
* - What the callbacks change through edit2D or edit3D is one undo: the editor ends the stroke at the release,
|
|
33697
|
+
* and takes it back when the right button or Escape ends the press
|
|
33698
|
+
* @param {string} name
|
|
33699
|
+
* @param {EditorTool} tool - {key, hint, onPress, onDrag, onRelease, onDraw}, each optional; onPress returning
|
|
33700
|
+
* false says the press was not the tool's */
|
|
33701
|
+
addTool(name, tool)
|
|
33702
|
+
{
|
|
33703
|
+
ASSERT(!!tool && typeof tool == 'object', 'a tool is an object of callbacks');
|
|
33704
|
+
const before = this.tools[name]; // added again, it takes the place of the first, its key too
|
|
33705
|
+
before?.key && delete this.keys[editorKeySpelling(before.key)];
|
|
33706
|
+
this.tools[name] = tool;
|
|
33707
|
+
tool.key && this.addKey(tool.key, ()=> { this.tool = this.tool === name ? undefined : name; });
|
|
33708
|
+
}
|
|
32895
33709
|
|
|
32896
33710
|
/** Open the editor, pausing the game; until close(), Escape (the debug key) switches between playing and editing */
|
|
32897
33711
|
open()
|
|
32898
33712
|
{
|
|
33713
|
+
if (editorOther?.wanted())
|
|
33714
|
+
return editorOther.open();
|
|
32899
33715
|
if (!editorSession)
|
|
32900
33716
|
editorCameraScale = cameraScale; // a new session starts at the game's zoom, a return keeps the editor's
|
|
32901
33717
|
editorSession = true;
|
|
@@ -32906,6 +33722,8 @@ class LevelEditor
|
|
|
32906
33722
|
* overlay again */
|
|
32907
33723
|
close()
|
|
32908
33724
|
{
|
|
33725
|
+
if (editorOther?.active())
|
|
33726
|
+
return editorOther.close();
|
|
32909
33727
|
editorSession = false;
|
|
32910
33728
|
editorSetOpen(false);
|
|
32911
33729
|
}
|
|
@@ -32914,7 +33732,118 @@ class LevelEditor
|
|
|
32914
33732
|
/** The level editor, levelEditor.open() to edit the level, levelEditor.close() to play on with the changes
|
|
32915
33733
|
* @type {LevelEditor}
|
|
32916
33734
|
* @memberof Editor */
|
|
32917
|
-
|
|
33735
|
+
let levelEditor = new LevelEditor;
|
|
33736
|
+
|
|
33737
|
+
/** Make a level editor of the game's own the one in use: an instance of a class that extends LevelEditor, with
|
|
33738
|
+
* the game's hooks as its methods; set it before the editor opens
|
|
33739
|
+
* @param {LevelEditor} editor
|
|
33740
|
+
* @memberof Editor */
|
|
33741
|
+
function setLevelEditor(editor)
|
|
33742
|
+
{
|
|
33743
|
+
ASSERT(editor instanceof LevelEditor, 'a level editor extends LevelEditor');
|
|
33744
|
+
levelEditor.isOpen && console.warn('setLevelEditor: the editor is open, close it first');
|
|
33745
|
+
levelEditor = editor;
|
|
33746
|
+
}
|
|
33747
|
+
|
|
33748
|
+
// the other editor, the 3D one, when its plugin is there: it says if it is the one wanted and open, opens and
|
|
33749
|
+
// closes, and has its edit functions; the 2D editor is this file
|
|
33750
|
+
/** @type {{wanted: function(): boolean, isOpen: function(): boolean, active: function(): boolean, open: Function,
|
|
33751
|
+
* close: Function, hasKey: function(string, boolean): boolean, edit: EditorEdit3D}|undefined} */
|
|
33752
|
+
let editorOther;
|
|
33753
|
+
|
|
33754
|
+
// if the game gave a hook: set on the editor, or a method of its own class, not the empty one of LevelEditor
|
|
33755
|
+
function editorHas(name)
|
|
33756
|
+
{
|
|
33757
|
+
const hook = levelEditor[name];
|
|
33758
|
+
return typeof hook == 'function' && hook !== LevelEditor.prototype[name];
|
|
33759
|
+
}
|
|
33760
|
+
|
|
33761
|
+
// a hook or a moment of the game's, called when it is one: a game may set one to undefined to take it away
|
|
33762
|
+
function editorCall(name, ...args)
|
|
33763
|
+
{
|
|
33764
|
+
const hook = levelEditor[name];
|
|
33765
|
+
return typeof hook == 'function' ? hook.apply(levelEditor, args) : undefined;
|
|
33766
|
+
}
|
|
33767
|
+
|
|
33768
|
+
// the names keys have, by their lower case, so a game may spell one in any case
|
|
33769
|
+
const editorKeyNames = {};
|
|
33770
|
+
for (const name of ['Delete', 'Backspace', 'Enter', 'Tab', 'Escape', 'Space', 'Home', 'End', 'PageUp', 'PageDown',
|
|
33771
|
+
'Insert', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ...[...Array(12)].map((v, i)=> 'F' + (i + 1))])
|
|
33772
|
+
editorKeyNames[name.toLowerCase()] = name;
|
|
33773
|
+
|
|
33774
|
+
// a key as addKey keeps it: a letter in lower case, a name as keys are named, 'ctrl+' in front with Ctrl
|
|
33775
|
+
function editorKeySpelling(key)
|
|
33776
|
+
{
|
|
33777
|
+
const text = String(key), ctrl = text.trim().toLowerCase().startsWith('ctrl+');
|
|
33778
|
+
let name = ctrl ? text.trim().slice(5) : text;
|
|
33779
|
+
name = name === ' ' ? 'Space' : name.trim();
|
|
33780
|
+
name = name.length === 1 ? name.toLowerCase() : editorKeyNames[name.toLowerCase()] ?? name;
|
|
33781
|
+
return (ctrl ? 'ctrl+' : '') + name;
|
|
33782
|
+
}
|
|
33783
|
+
|
|
33784
|
+
// say once, for each key of the game's own that is one of the open editor's too, that the game's takes its place:
|
|
33785
|
+
// when the editor opens, or when the key is added while it is open, since the 2D and 3D editors' keys differ
|
|
33786
|
+
function editorKeyClashes()
|
|
33787
|
+
{
|
|
33788
|
+
if (!levelEditor.isOpen) return;
|
|
33789
|
+
for (const [spelled, key] of Object.entries(levelEditor.keys))
|
|
33790
|
+
{
|
|
33791
|
+
const ctrl = spelled.startsWith('ctrl+'), name = spelled.slice(ctrl ? 5 : 0);
|
|
33792
|
+
// in 2D the digits 1 to 9 pick a layer
|
|
33793
|
+
const clash = levelEditor.is3D ? editorOther.hasKey(name, ctrl) :
|
|
33794
|
+
!!(ctrl ? editorCtrlKeys : editorKeys)[name] || !ctrl && name.length === 1 && name >= '1' && name <= '9';
|
|
33795
|
+
if (key.warned || !clash) continue;
|
|
33796
|
+
key.warned = true;
|
|
33797
|
+
console.warn(`levelEditor.addKey: ${spelled} is a key of the editor's, the game's takes its place`);
|
|
33798
|
+
}
|
|
33799
|
+
}
|
|
33800
|
+
|
|
33801
|
+
// the help lines of the game's own keys
|
|
33802
|
+
function editorGameHelpLines()
|
|
33803
|
+
{ return Object.values(levelEditor.keys).map((key)=> key.help).filter((line)=> !!line); }
|
|
33804
|
+
|
|
33805
|
+
// the tool of the game's own that is on, undefined when there is none
|
|
33806
|
+
function editorGameTool() { return levelEditor.tool !== undefined ? levelEditor.tools[levelEditor.tool] : undefined; }
|
|
33807
|
+
|
|
33808
|
+
// the game's box of an editor's panel: a row of its tools and its buttons, made again when one was added, the
|
|
33809
|
+
// tool that is on lit; and, the first time for a level editor, onPanel is given the box for controls of its own
|
|
33810
|
+
const editorGamePanels = new WeakMap;
|
|
33811
|
+
function editorGamePanel(box)
|
|
33812
|
+
{
|
|
33813
|
+
let state = editorGamePanels.get(box);
|
|
33814
|
+
if (state?.owner !== levelEditor)
|
|
33815
|
+
{
|
|
33816
|
+
box.replaceChildren();
|
|
33817
|
+
state = {owner: levelEditor, row: editorElement('div', box, 'display:flex;gap:4px;margin:4px 0;flex-wrap:wrap'),
|
|
33818
|
+
key: '', buttons: {}};
|
|
33819
|
+
editorGamePanels.set(box, state);
|
|
33820
|
+
editorCall('onPanel', box);
|
|
33821
|
+
}
|
|
33822
|
+
const tools = Object.keys(levelEditor.tools);
|
|
33823
|
+
const key = tools.map((name)=> name + '\t' + (levelEditor.tools[name].hint || '')).join('\n') + '\n\n' +
|
|
33824
|
+
levelEditor.buttons.length;
|
|
33825
|
+
if (state.key !== key)
|
|
33826
|
+
{
|
|
33827
|
+
state.key = key;
|
|
33828
|
+
state.buttons = {};
|
|
33829
|
+
const press = 'padding:3px 6px;cursor:pointer';
|
|
33830
|
+
state.row.replaceChildren(...tools.map((name)=>
|
|
33831
|
+
{
|
|
33832
|
+
const b = state.buttons[name] = editorElement('button', undefined, press, name);
|
|
33833
|
+
b.title = levelEditor.tools[name].hint || '';
|
|
33834
|
+
b.onclick = ()=> { levelEditor.tool = levelEditor.tool === name ? undefined : name; b.blur?.(); };
|
|
33835
|
+
return b;
|
|
33836
|
+
}), ...levelEditor.buttons.map((button)=>
|
|
33837
|
+
{
|
|
33838
|
+
const b = editorElement('button', undefined, press, button.label);
|
|
33839
|
+
b.title = button.title;
|
|
33840
|
+
b.onclick = ()=> { button.onClick.call(levelEditor); b.blur?.(); };
|
|
33841
|
+
return b;
|
|
33842
|
+
}));
|
|
33843
|
+
}
|
|
33844
|
+
for (const name of tools)
|
|
33845
|
+
state.buttons[name].style.outline = name === levelEditor.tool ? '2px solid #4af' : '';
|
|
33846
|
+
}
|
|
32918
33847
|
|
|
32919
33848
|
// if the editor is open, and if it was opened and not exited, while Escape switches between playing and editing
|
|
32920
33849
|
let editorIsOpen = false, editorSession = false;
|
|
@@ -32926,7 +33855,7 @@ let editorPlayFromMouse = false, editorAdvanced = false;
|
|
|
32926
33855
|
function editorPlay(pos)
|
|
32927
33856
|
{
|
|
32928
33857
|
editorSetOpen(false);
|
|
32929
|
-
editorPlayFromMouse && pos && levelEditor.onPlayFrom
|
|
33858
|
+
editorPlayFromMouse && pos && editorHas('onPlayFrom') && levelEditor.onPlayFrom(pos.copy());
|
|
32930
33859
|
}
|
|
32931
33860
|
|
|
32932
33861
|
// the game's pause and camera from before the editor opened, handed back when it closes
|
|
@@ -32978,10 +33907,13 @@ function editorSetOpen(open)
|
|
|
32978
33907
|
layers.filter((layer)=> !layer.isObjects).at(-1));
|
|
32979
33908
|
layers.includes(editorObjectLayer) || (editorObjectLayer = editorLayer ? undefined :
|
|
32980
33909
|
layers.find((layer)=> layer.isObjects)); // a level of objects alone
|
|
33910
|
+
editorCall('onOpen');
|
|
33911
|
+
editorKeyClashes();
|
|
32981
33912
|
}
|
|
32982
33913
|
else
|
|
32983
33914
|
{
|
|
32984
33915
|
editorStrokeEnd();
|
|
33916
|
+
editorToolHeld = false;
|
|
32985
33917
|
editorSelection = editorSelectionDrag = editorRightPress = editorObjectDrag = editorObjectBox = undefined;
|
|
32986
33918
|
editorObjectSelection.clear();
|
|
32987
33919
|
editorMouseOnPanel = false; // the panel hides, with no mouseleave
|
|
@@ -32990,6 +33922,7 @@ function editorSetOpen(open)
|
|
|
32990
33922
|
setCameraPos(state.cameraPos);
|
|
32991
33923
|
setCameraScale(state.cameraScale);
|
|
32992
33924
|
setCameraAngle(state.cameraAngle);
|
|
33925
|
+
editorCall('onClose');
|
|
32993
33926
|
}
|
|
32994
33927
|
}
|
|
32995
33928
|
|
|
@@ -33260,7 +34193,14 @@ async function editorSave(record, pickAgain=false)
|
|
|
33260
34193
|
if (!record) return;
|
|
33261
34194
|
editorStrokeEnd();
|
|
33262
34195
|
const text = editorMapJSON(record); // the map as it is now, later edits wait for a later save
|
|
33263
|
-
|
|
34196
|
+
// its place in line is taken now, and the game's hook is asked when its turn comes, so saves are written in
|
|
34197
|
+
// the order they were asked for however long a hook takes
|
|
34198
|
+
const saved = (async ()=>
|
|
34199
|
+
{
|
|
34200
|
+
await record.saving;
|
|
34201
|
+
if (await editorCall('onSave', text, record.fileName) === true) return 'kept'; // the game kept it itself
|
|
34202
|
+
return editorSaveText(record, text, pickAgain);
|
|
34203
|
+
})();
|
|
33264
34204
|
record.saving = saved.catch(()=> {});
|
|
33265
34205
|
return saved;
|
|
33266
34206
|
}
|
|
@@ -33384,7 +34324,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
33384
34324
|
editorStrokeEnd();
|
|
33385
34325
|
if (width !== record.map.width || height !== record.map.height)
|
|
33386
34326
|
{
|
|
33387
|
-
if (!
|
|
34327
|
+
if (!editorHas('onRestart')) return;
|
|
33388
34328
|
const before = editorMapSnapshot(record);
|
|
33389
34329
|
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
33390
34330
|
editorMapChanged(record, before);
|
|
@@ -33429,7 +34369,7 @@ function editorPendingFits(record, saved)
|
|
|
33429
34369
|
{
|
|
33430
34370
|
const map = record.map, data = editorTileLayerData(map.layers), groups = editorObjectGroups(map.layers).length;
|
|
33431
34371
|
const width = saved.width ?? map.width, height = saved.height ?? map.height;
|
|
33432
|
-
return (width === map.width && height === map.height ||
|
|
34372
|
+
return (width === map.width && height === map.height || editorHas('onRestart')) &&
|
|
33433
34373
|
saved.layers?.length === data.length && saved.layers.every((layer)=> layer.length === width * height) &&
|
|
33434
34374
|
(saved.objects ?? []).every((objects, i)=> i < groups || !objects.length);
|
|
33435
34375
|
}
|
|
@@ -33533,7 +34473,7 @@ function editorMapChanged(record, before)
|
|
|
33533
34473
|
function editorResize(record, width, height)
|
|
33534
34474
|
{
|
|
33535
34475
|
const map = record?.map;
|
|
33536
|
-
if (!map || record.synthetic || record.pending || !
|
|
34476
|
+
if (!map || record.synthetic || record.pending || !editorHas('onRestart')) return false;
|
|
33537
34477
|
if (width === map.width && height === map.height) return false;
|
|
33538
34478
|
editorStrokeEnd();
|
|
33539
34479
|
const before = editorMapSnapshot(record);
|
|
@@ -33587,7 +34527,7 @@ function editorSetCell(layer, pos, gid)
|
|
|
33587
34527
|
}
|
|
33588
34528
|
const t = editorGidToTile(gid);
|
|
33589
34529
|
live.setData(pos, t ? new TileLayerData(t.tile, t.direction, t.mirror, color) : new TileLayerData, true);
|
|
33590
|
-
if (
|
|
34530
|
+
if (editorHas('onTile'))
|
|
33591
34531
|
levelEditor.onTile(live, pos.copy(), t?.tile);
|
|
33592
34532
|
else if (live instanceof TileCollisionLayer && live.isSolid)
|
|
33593
34533
|
live.setCollisionData(pos, gid ? 1 : 0); // as tileLayersLoad gives it
|
|
@@ -33656,7 +34596,7 @@ function editorChanged(stroke)
|
|
|
33656
34596
|
editorRedraw(stroke);
|
|
33657
34597
|
for (const record of new Set(stroke.map(editorEntryRecord)))
|
|
33658
34598
|
editorAutosave(record);
|
|
33659
|
-
stroke.some((entry)=> entry.resize) && levelEditor.onRestart
|
|
34599
|
+
stroke.some((entry)=> entry.resize) && editorHas('onRestart') && levelEditor.onRestart();
|
|
33660
34600
|
}
|
|
33661
34601
|
|
|
33662
34602
|
// the tile layers a stroke touched draw again whole
|
|
@@ -33849,6 +34789,7 @@ let editorObjectDrag, editorObjectBox;
|
|
|
33849
34789
|
function editorSelectLayer(layer)
|
|
33850
34790
|
{
|
|
33851
34791
|
editorStrokeEnd();
|
|
34792
|
+
levelEditor.tool = undefined; // a tool of the game's own is put down
|
|
33852
34793
|
editorLastCell = editorObjectDrag = editorRightPress = editorObjectBox = undefined; // presses stay on their layer
|
|
33853
34794
|
editorSelectionDrag = undefined; // a move so far is kept, the stroke ended
|
|
33854
34795
|
editorObjectSelection.clear();
|
|
@@ -34476,6 +35417,7 @@ const editorHelpLines =
|
|
|
34476
35417
|
function editorHint()
|
|
34477
35418
|
{
|
|
34478
35419
|
if (keyIsDown('Space')) return 'Drag to pan';
|
|
35420
|
+
if (editorGameTool()) return editorGameTool().hint || levelEditor.tool;
|
|
34479
35421
|
if (editorObjectLayer)
|
|
34480
35422
|
return editorObjectSelection.size ? 'Selection: drag moves · Delete removes · Ctrl+C copy · Ctrl+X cut · click to clear' :
|
|
34481
35423
|
'Left place / select · drag moves · Right pick / drag select · Delete removes';
|
|
@@ -34544,6 +35486,8 @@ function editorPanelInit()
|
|
|
34544
35486
|
button(undo, 'Undo', ()=> editorUndo(), 'Ctrl+Z');
|
|
34545
35487
|
button(undo, 'Redo', ()=> editorUndo(true), 'Ctrl+Y');
|
|
34546
35488
|
button(undo, 'Keys', ()=> editorHelp = !editorHelp, 'Every control, ?');
|
|
35489
|
+
// the game's own tools, buttons and controls, near the top where the tools are
|
|
35490
|
+
const game = editorElement('div', editorPanel);
|
|
34547
35491
|
|
|
34548
35492
|
// a file that changed under its autosave
|
|
34549
35493
|
const pending = editorElement('div', editorPanel, 'padding:4px;margin:4px 0;background:#630;border-radius:3px');
|
|
@@ -34630,9 +35574,10 @@ function editorPanelInit()
|
|
|
34630
35574
|
const help = editorElement('div', editorPanel, 'color:#aaa;margin-top:4px;border-top:1px solid #444;padding-top:4px');
|
|
34631
35575
|
for (const line of editorHelpLines)
|
|
34632
35576
|
editorElement('div', help, 'margin:2px 0', line);
|
|
35577
|
+
const gameHelp = editorElement('div', help);
|
|
34633
35578
|
button(help, 'Close', ()=> editorHelp = false, '?');
|
|
34634
35579
|
|
|
34635
|
-
editorPanelParts = {pendingUnfit, reset, advancedToggle, advanced, playFromLabel, playFrom, sizeRow, sizeX, sizeY, resize, restart, pending, layerRow, allLayers, turns, palette, brush, properties, status, storage, hint, help, layers: undefined};
|
|
35580
|
+
editorPanelParts = {game, gameHelp, pendingUnfit, reset, advancedToggle, advanced, playFromLabel, playFrom, sizeRow, sizeX, sizeY, resize, restart, pending, layerRow, allLayers, turns, palette, brush, properties, status, storage, hint, help, layers: undefined};
|
|
34636
35581
|
}
|
|
34637
35582
|
|
|
34638
35583
|
// the palette's cell size in pixels and how many to a row
|
|
@@ -34791,15 +35736,15 @@ function editorPanelUpdate()
|
|
|
34791
35736
|
p.hint.textContent = editorHint();
|
|
34792
35737
|
p.help.style.display = editorHelp ? '' : 'none';
|
|
34793
35738
|
p.storage.style.display = editorSaveFailed ? '' : 'none';
|
|
34794
|
-
p.restart.style.display =
|
|
35739
|
+
p.restart.style.display = editorHas('onRestart') ? '' : 'none';
|
|
34795
35740
|
p.advancedToggle.textContent = editorAdvanced ? 'Advanced ▾' : 'Advanced ▸';
|
|
34796
35741
|
p.advanced.style.display = editorAdvanced ? '' : 'none';
|
|
34797
|
-
p.playFromLabel.style.display =
|
|
35742
|
+
p.playFromLabel.style.display = editorHas('onPlayFrom') ? 'flex' : 'none';
|
|
34798
35743
|
p.playFrom.checked = editorPlayFromMouse;
|
|
34799
35744
|
|
|
34800
35745
|
// the level's size, shown until a field is being typed in; without the Restart hook it can not change
|
|
34801
35746
|
const record = editorRecord(), map = record?.map;
|
|
34802
|
-
const canResize =
|
|
35747
|
+
const canResize = editorHas('onRestart') && !record?.synthetic && !record?.pending;
|
|
34803
35748
|
p.sizeRow.style.display = map ? 'flex' : 'none';
|
|
34804
35749
|
if (map && !p.sizeRow.contains(document.activeElement))
|
|
34805
35750
|
{
|
|
@@ -34869,6 +35814,7 @@ function editorIsTextField(target)
|
|
|
34869
35814
|
function editorOnKeyDown(e)
|
|
34870
35815
|
{
|
|
34871
35816
|
if (!editorIsOpen || e.repeat || e.altKey || editorIsTextField(e.target)) return;
|
|
35817
|
+
if (editorToolHeld) return; // the keys wait for a tool of the game's own to let go
|
|
34872
35818
|
if (editorSelectionDrag && !(debugKey && e.code === debugKey))
|
|
34873
35819
|
{
|
|
34874
35820
|
// a key ends a selection drag where it is, an undo of its own, so Delete or undo acts on what is there;
|
|
@@ -34876,10 +35822,12 @@ function editorOnKeyDown(e)
|
|
|
34876
35822
|
editorStrokeEnd();
|
|
34877
35823
|
editorSelectionDrag = undefined;
|
|
34878
35824
|
}
|
|
34879
|
-
let key = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
34880
|
-
if (key.length === 1 && !/[a-z?]/.test(key))
|
|
35825
|
+
let key = e.key === ' ' ? 'Space' : e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
|
35826
|
+
if (key.length === 1 && !/[a-z?]/.test(key) && !levelEditor.keys[key])
|
|
34881
35827
|
key = e.code?.match(/^Key([A-Z])$/)?.[1].toLowerCase() ?? key;
|
|
34882
|
-
|
|
35828
|
+
// a key of the game's own first, it takes the place of one of the editor's
|
|
35829
|
+
const ctrl = e.ctrlKey || e.metaKey, own = levelEditor.keys[(ctrl ? 'ctrl+' : '') + key];
|
|
35830
|
+
const action = own ? (shift)=> own.action.call(levelEditor, shift) : (ctrl ? editorCtrlKeys : editorKeys)[key];
|
|
34883
35831
|
if (action && action(e.shiftKey) !== false)
|
|
34884
35832
|
e.preventDefault();
|
|
34885
35833
|
}
|
|
@@ -35073,6 +36021,15 @@ function editorUpdate()
|
|
|
35073
36021
|
return;
|
|
35074
36022
|
}
|
|
35075
36023
|
|
|
36024
|
+
// Escape while a tool of the game's own is held takes its press back, and stays in the editor
|
|
36025
|
+
if (editorToolHeld && debugKey && keyWasPressed(debugKey))
|
|
36026
|
+
{
|
|
36027
|
+
inputClearKey(debugKey);
|
|
36028
|
+
editorStrokeCancel();
|
|
36029
|
+
editorToolHeld = false;
|
|
36030
|
+
return;
|
|
36031
|
+
}
|
|
36032
|
+
|
|
35076
36033
|
// in an editing session Escape, the debug key, switches between playing and editing; taken, so the debug
|
|
35077
36034
|
// overlay waits till the session ends
|
|
35078
36035
|
if (editorSession && debugKey && keyWasPressed(debugKey))
|
|
@@ -35103,7 +36060,7 @@ function editorUpdate()
|
|
|
35103
36060
|
// keeps them while it is open
|
|
35104
36061
|
for (let i = 1; !debugOverlay && i <= 9; ++i)
|
|
35105
36062
|
{
|
|
35106
|
-
if (!keyWasPressed('Digit' + i)) continue;
|
|
36063
|
+
if (!keyWasPressed('Digit' + i) || levelEditor.keys[i]) continue; // a digit of the game's own is its
|
|
35107
36064
|
inputClearKey('Digit' + i);
|
|
35108
36065
|
const layer = editorLayers()[i - 1];
|
|
35109
36066
|
layer && layer !== (editorObjectLayer ?? editorLayer) && editorSelectLayer(layer); // a drag does not carry across
|
|
@@ -35116,6 +36073,8 @@ function editorUpdate()
|
|
|
35116
36073
|
editorApplyCamera();
|
|
35117
36074
|
|
|
35118
36075
|
editorMouseOnPanel || (editorMouseScreen = mousePosScreen.copy());
|
|
36076
|
+
editorCall('onUpdate');
|
|
36077
|
+
if (editorToolUpdate(space)) return; // a tool of the game's own has the mouse
|
|
35119
36078
|
if (editorObjectLayer)
|
|
35120
36079
|
return editorUpdateObjects(space);
|
|
35121
36080
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer, mouse = screenToWorld(editorMouseScreen);
|
|
@@ -35185,10 +36144,87 @@ function editorUpdate()
|
|
|
35185
36144
|
}
|
|
35186
36145
|
}
|
|
35187
36146
|
|
|
36147
|
+
// a tool of the game's own: if its press is held, and what was picked in the editor when it was turned on, since
|
|
36148
|
+
// picking a layer, a tile or an object type puts the game's tool down
|
|
36149
|
+
let editorToolHeld = false, editorToolName, editorToolPicked = [];
|
|
36150
|
+
|
|
36151
|
+
// what a tool's callbacks are given: where the mouse is in the level, the cell under it on the selected tile
|
|
36152
|
+
// layer, and the keys held
|
|
36153
|
+
function editorToolAt()
|
|
36154
|
+
{
|
|
36155
|
+
const layer = editorObjectLayer || editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
36156
|
+
const pos = screenToWorld(editorMouseScreen);
|
|
36157
|
+
return {pos, cell: layer && !editorMouseOnPanel ? editorCellAt(layer.live, pos) || undefined : undefined,
|
|
36158
|
+
ray: undefined, shift: keyIsDown('ShiftLeft') || keyIsDown('ShiftRight'),
|
|
36159
|
+
ctrl: keyIsDown('ControlLeft') || keyIsDown('ControlRight')};
|
|
36160
|
+
}
|
|
36161
|
+
|
|
36162
|
+
// the game's tool, when one is on: the left button is its, a press, a drag and a release one undo, the right
|
|
36163
|
+
// button takes a held press back; true when it has the mouse
|
|
36164
|
+
function editorToolUpdate(space)
|
|
36165
|
+
{
|
|
36166
|
+
const picked = [editorLayer, editorObjectLayer, editorBrush, editorObjectBrush];
|
|
36167
|
+
if (levelEditor.tool !== editorToolName)
|
|
36168
|
+
editorToolName = levelEditor.tool, editorToolPicked = picked; // turned on, or off, by its key or button
|
|
36169
|
+
else if (picked.some((value, i)=> value !== editorToolPicked[i]))
|
|
36170
|
+
levelEditor.tool = editorToolName = undefined; // something of the editor's was picked
|
|
36171
|
+
const tool = editorGameTool();
|
|
36172
|
+
if (!tool)
|
|
36173
|
+
{
|
|
36174
|
+
editorToolHeld = false;
|
|
36175
|
+
return false;
|
|
36176
|
+
}
|
|
36177
|
+
const at = editorToolAt();
|
|
36178
|
+
editorHover = at.cell;
|
|
36179
|
+
editorRightPress = undefined; // a right press from before the tool was on is not the editor's to finish
|
|
36180
|
+
if (editorToolHeld && mouseWasPressed(2))
|
|
36181
|
+
{
|
|
36182
|
+
inputClearKey(2);
|
|
36183
|
+
editorStrokeCancel();
|
|
36184
|
+
editorToolHeld = false;
|
|
36185
|
+
}
|
|
36186
|
+
else if (mouseWasPressed(0) && !space && !editorMouseOnPanel)
|
|
36187
|
+
editorToolHeld = tool.onPress?.(at) !== false;
|
|
36188
|
+
else if (editorToolHeld && mouseIsDown(0))
|
|
36189
|
+
tool.onDrag?.(at);
|
|
36190
|
+
if (editorToolHeld && !mouseIsDown(0))
|
|
36191
|
+
{
|
|
36192
|
+
tool.onRelease?.(at);
|
|
36193
|
+
editorStrokeEnd();
|
|
36194
|
+
editorToolHeld = false;
|
|
36195
|
+
}
|
|
36196
|
+
return true;
|
|
36197
|
+
}
|
|
36198
|
+
|
|
36199
|
+
// the 2D editor's edit functions, what levelEditor.edit2D gives a game's own keys, buttons and tools
|
|
36200
|
+
const editorEdit2D =
|
|
36201
|
+
{
|
|
36202
|
+
get map() { return editorRecord()?.map; },
|
|
36203
|
+
get layer() { return editorObjectLayer ? undefined : editorLayer?.live; },
|
|
36204
|
+
get objects() { return editorObjectLayer ? editorObjectsCopy(editorObjectList(editorObjectLayer)) : []; },
|
|
36205
|
+
get hover() { return editorHover && !editorObjectLayer ? editorHover.copy() : undefined; },
|
|
36206
|
+
get selection() { return editorObjectSelection; },
|
|
36207
|
+
paint(cell, tile, direction=0, mirror=false)
|
|
36208
|
+
{
|
|
36209
|
+
const layer = editorObjectLayer ? undefined : editorLayer, size = layer?.live.size;
|
|
36210
|
+
if (!layer || layer.live.destroyed || layer.record.pending || !isVector2(cell)) return false;
|
|
36211
|
+
const x = floor(cell.x), y = floor(cell.y);
|
|
36212
|
+
if (x < 0 || y < 0 || x >= size.x || y >= size.y) return false;
|
|
36213
|
+
editorPaint(layer, vec2(x, y), tile < 0 ? 0 : editorTileToGid(tile, direction & 3, mirror));
|
|
36214
|
+
return true;
|
|
36215
|
+
},
|
|
36216
|
+
changeObjects(change) { return editorChangeObjects(editorObjectLayer, change); },
|
|
36217
|
+
strokeEnd() { editorStrokeEnd(); },
|
|
36218
|
+
strokeCancel() { editorStrokeCancel(); },
|
|
36219
|
+
bulk(edit) { editorBulkEdit(edit); },
|
|
36220
|
+
undo(redo=false) { return editorUndo(redo); },
|
|
36221
|
+
toJSON() { const record = editorRecord(); return record ? editorMapJSON(record) : ''; },
|
|
36222
|
+
};
|
|
36223
|
+
|
|
35188
36224
|
// switch to play and have the game rebuild its level from the changed map, when it has a hook for that
|
|
35189
36225
|
function editorRestart()
|
|
35190
36226
|
{
|
|
35191
|
-
if (!
|
|
36227
|
+
if (!editorHas('onRestart')) return;
|
|
35192
36228
|
editorSetOpen(false); // ends a held stroke, and hands back the game's pause and camera, Escape comes back
|
|
35193
36229
|
levelEditor.onRestart();
|
|
35194
36230
|
}
|
|
@@ -35202,6 +36238,11 @@ function editorPreRender() { editorIsOpen && editorApplyCamera(); }
|
|
|
35202
36238
|
function editorRender()
|
|
35203
36239
|
{
|
|
35204
36240
|
headlessMode || editorPanelUpdate();
|
|
36241
|
+
if (!headlessMode && editorIsOpen && editorPanelParts)
|
|
36242
|
+
{
|
|
36243
|
+
editorGamePanel(editorPanelParts.game);
|
|
36244
|
+
editorGameHelp(editorPanelParts.gameHelp);
|
|
36245
|
+
}
|
|
35205
36246
|
if (!editorIsOpen || headlessMode) return;
|
|
35206
36247
|
const layer = editorLayer?.live.destroyed ? undefined : editorLayer;
|
|
35207
36248
|
if (!layer && !editorObjectLayer) return;
|
|
@@ -35250,12 +36291,12 @@ function editorRender()
|
|
|
35250
36291
|
if (editorSelection && !editorObjectLayer)
|
|
35251
36292
|
outline(editorSelection.min, editorSelection.max.add(vec2(1)), hsl(.15, 1, .6), thin * 3);
|
|
35252
36293
|
const last = editorLastPlaced, shift = keyIsDown('ShiftLeft') || keyIsDown('ShiftRight');
|
|
35253
|
-
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0))
|
|
36294
|
+
if (shift && editorHover && last?.layer === layer && !mouseIsDown(0) && !editorGameTool())
|
|
35254
36295
|
drawLine(live.pos.add(last.pos).add(vec2(.5)), live.pos.add(editorHover).add(vec2(.5)), thin * 2,
|
|
35255
36296
|
hsl(.55, 1, .6, .6), undefined, 0, glEnable, false);
|
|
35256
36297
|
|
|
35257
|
-
// the brush where it would paint, the Erase brush's cells in red
|
|
35258
|
-
if (editorHover)
|
|
36298
|
+
// the brush where it would paint, the Erase brush's cells in red; a tool of the game's own draws its own
|
|
36299
|
+
if (editorHover && !editorGameTool())
|
|
35259
36300
|
{
|
|
35260
36301
|
const grid = editorStampGrid(layer, editorBrush), {width: w, height: h} = editorBrush;
|
|
35261
36302
|
for (let y = h; y--;)
|
|
@@ -35273,7 +36314,24 @@ function editorRender()
|
|
|
35273
36314
|
editorObjectLayer && editorRenderObjects(thin);
|
|
35274
36315
|
}
|
|
35275
36316
|
|
|
35276
|
-
|
|
36317
|
+
// the game's own drawing in the level, over the editor's: the editor's onDraw, and its tool's while one is on
|
|
36318
|
+
function editorRenderGame()
|
|
36319
|
+
{
|
|
36320
|
+
if (!editorIsOpen) return;
|
|
36321
|
+
editorCall('onDraw');
|
|
36322
|
+
editorGameTool()?.onDraw?.(editorToolAt());
|
|
36323
|
+
}
|
|
36324
|
+
|
|
36325
|
+
// the lines of the game's own keys in a panel's help, made again when they change
|
|
36326
|
+
function editorGameHelp(element)
|
|
36327
|
+
{
|
|
36328
|
+
const lines = editorGameHelpLines(), key = lines.join('\n');
|
|
36329
|
+
if (element.dataset.key === key) return;
|
|
36330
|
+
element.dataset.key = key;
|
|
36331
|
+
element.replaceChildren(...lines.map((line)=> editorElement('div', undefined, 'margin:2px 0', line)));
|
|
36332
|
+
}
|
|
36333
|
+
|
|
36334
|
+
debug && engineAddPlugin(editorUpdate, ()=> { editorRender(); editorRenderGame(); });
|
|
35277
36335
|
|
|
35278
36336
|
// the editor's own listeners for letter shortcuts and the wheel, the engine's input reads keys by position
|
|
35279
36337
|
if (debug && globalThis.document?.addEventListener)
|
|
@@ -35570,9 +36628,11 @@ const editor3DSelection = new Set; // the ids of the selected objects
|
|
|
35570
36628
|
// the type a click places
|
|
35571
36629
|
/** @type {string|undefined} */
|
|
35572
36630
|
let editor3DBrush;
|
|
35573
|
-
// the copied objects
|
|
36631
|
+
// the copied objects, and the size and box offset each had when it was copied
|
|
35574
36632
|
/** @type {Array<Object>|undefined} */
|
|
35575
36633
|
let editor3DClipboard;
|
|
36634
|
+
/** @type {Map<Object, {size: Vector3, offset: Vector3}>} */
|
|
36635
|
+
let editor3DClipboardBoxes = new Map;
|
|
35576
36636
|
// the level's, each entry its object list and its parts, the scene and the blocks, before and after an edit
|
|
35577
36637
|
let editor3DUndoList = [], editor3DRedoList = [];
|
|
35578
36638
|
// the edit being made, a drag is one: the objects and the parts as they were before it, and painted when blocks
|
|
@@ -35591,7 +36651,7 @@ const editor3DSelected = ()=> editor3DObjects().filter((o)=> editor3DSelection.h
|
|
|
35591
36651
|
|
|
35592
36652
|
// the parts of a level beside its objects that the editor edits: its scene block and its map of blocks; each is
|
|
35593
36653
|
// undone, autosaved, reset and saved as the objects are
|
|
35594
|
-
const editor3DLevelPartNames = ['scene', 'voxels', 'terrain'];
|
|
36654
|
+
const editor3DLevelPartNames = ['scene', 'voxels', 'terrain', 'prefabs'];
|
|
35595
36655
|
|
|
35596
36656
|
// the last copy made of each part, as its text and the copy: a part that has not changed since is not copied
|
|
35597
36657
|
// again, so undo steps share one copy of a terrain until it is sculpted; a copy is never changed
|
|
@@ -35695,12 +36755,46 @@ const editor3DTurned = (object)=> editor3DRotation(object).lengthSquared() > 0;
|
|
|
35695
36755
|
// the size of an object's box: what the game made for it says, a marker is 1 unit
|
|
35696
36756
|
function editor3DSize(object)
|
|
35697
36757
|
{
|
|
35698
|
-
const made = editor3DInstances.get(object.id);
|
|
36758
|
+
const made = editor3DInstances.get(object.id), box = editor3DPrefabBox(made);
|
|
36759
|
+
if (box) return box.size;
|
|
35699
36760
|
if (!(made instanceof EngineObject3D) || !(made.mesh || made.tileInfo)) return vec3(1);
|
|
35700
36761
|
const s = made.size3D, k = made.scale3D;
|
|
35701
36762
|
return vec3(s.x * abs(k.x), s.y * abs(k.y), s.z * abs(k.z));
|
|
35702
36763
|
}
|
|
35703
36764
|
|
|
36765
|
+
// the box around a prefab's instance in the world, the boxes of its parts together, as its middle and its size;
|
|
36766
|
+
// undefined for anything else, or an instance with nothing to see
|
|
36767
|
+
function editor3DPrefabBox(made)
|
|
36768
|
+
{
|
|
36769
|
+
if (!(made instanceof Prefab3D) || made.destroyed) return;
|
|
36770
|
+
const low = vec3(Infinity), high = vec3(-Infinity);
|
|
36771
|
+
for (const part of editor3DParts(made))
|
|
36772
|
+
{
|
|
36773
|
+
if (!(part instanceof EngineObject3D) || !(part.mesh || part.tileInfo)) continue;
|
|
36774
|
+
const bounds = part.mesh && (part.mesh.bounds || part.mesh.getBounds());
|
|
36775
|
+
const matrix = render3DObjectMatrix(part).copy().multiply(bounds ?
|
|
36776
|
+
buildMatrix(bounds.min.add(bounds.max).scale(.5), undefined, bounds.max.subtract(bounds.min)) :
|
|
36777
|
+
buildMatrix(vec3(), undefined, part.size3D));
|
|
36778
|
+
for (let i = 8; i--;)
|
|
36779
|
+
{
|
|
36780
|
+
const p = matrix.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
|
|
36781
|
+
for (const k of ['x', 'y', 'z'])
|
|
36782
|
+
low[k] = min(low[k], p[k]), high[k] = max(high[k], p[k]);
|
|
36783
|
+
}
|
|
36784
|
+
}
|
|
36785
|
+
// to the nearest part of the file's numbers, so a box worked out through a matrix reads as it was written
|
|
36786
|
+
const tidy = (v)=> vec3(editor3DRound(v.x), editor3DRound(v.y), editor3DRound(v.z));
|
|
36787
|
+
return low.x <= high.x ? {pos: tidy(low.add(high).scale(.5)), size: tidy(high.subtract(low))} : undefined;
|
|
36788
|
+
}
|
|
36789
|
+
|
|
36790
|
+
// from an object's place to the middle of its box: nothing for a plain object, whose place is its middle, and for
|
|
36791
|
+
// a prefab's instance how far its origin is from the middle of its parts, as it is turned and sized now
|
|
36792
|
+
function editor3DBoxOffset(object)
|
|
36793
|
+
{
|
|
36794
|
+
const box = editor3DPrefabBox(editor3DInstances.get(object.id));
|
|
36795
|
+
return box ? box.pos.subtract(editor3DPos(object)) : vec3();
|
|
36796
|
+
}
|
|
36797
|
+
|
|
35704
36798
|
// every object gets an id of its own, a whole number: one without, or with one already used, gets a new one
|
|
35705
36799
|
function editor3DFixIds(objects)
|
|
35706
36800
|
{
|
|
@@ -35736,6 +36830,12 @@ function editor3DLevelLoaded(level)
|
|
|
35736
36830
|
editor3DSelection.clear();
|
|
35737
36831
|
editor3DStroke = undefined;
|
|
35738
36832
|
}
|
|
36833
|
+
if (!editor3DPrefabLevels.has(level) && editor3DPrefabStack.length)
|
|
36834
|
+
{
|
|
36835
|
+
// the game loaded a level while a prefab was open: what was open is gone with what it was opened from
|
|
36836
|
+
editor3DPrefabStack.length = 0;
|
|
36837
|
+
editor3DPrefabEdits = {};
|
|
36838
|
+
}
|
|
35739
36839
|
editor3DLevel = level;
|
|
35740
36840
|
editor3DInstances.clear();
|
|
35741
36841
|
if (isArray(level.objects))
|
|
@@ -35775,7 +36875,8 @@ function editor3DLevelLoaded(level)
|
|
|
35775
36875
|
editor3DUndoList = record.undo;
|
|
35776
36876
|
editor3DRedoList = record.redo;
|
|
35777
36877
|
// an autosave the file already has, from a Save, goes; one of this file, or of the file a Save wrote, comes back
|
|
35778
|
-
|
|
36878
|
+
// a prefab being edited has no autosave, and one of a level with the same objects is not its
|
|
36879
|
+
const saved = editor3DPrefabLevels.has(level) ? undefined : editor3DSaves()[record.key];
|
|
35779
36880
|
if (saved && isArray(saved.objects) && editor3DSame(saved.objects, original) &&
|
|
35780
36881
|
editor3DSame(editor3DLevelParts(saved), originalParts))
|
|
35781
36882
|
editor3DAutosave(level);
|
|
@@ -35805,6 +36906,7 @@ function editor3DPlaceInstance(made, object)
|
|
|
35805
36906
|
made.scale3D = (level3DBaseScale.get(made) ?? vec3(1)).multiply(editor3DScale(object));
|
|
35806
36907
|
made.velocity3D = vec3();
|
|
35807
36908
|
made.angleVelocity3D = vec3();
|
|
36909
|
+
made instanceof Prefab3D && made.placeAt(editor3DPos(object)); // by its origin, and its parts go with it
|
|
35808
36910
|
}
|
|
35809
36911
|
|
|
35810
36912
|
// make an object's game object, again when it has one
|
|
@@ -35839,6 +36941,24 @@ function editor3DSetObjects(list)
|
|
|
35839
36941
|
made instanceof EngineObject3D && !made.destroyed;
|
|
35840
36942
|
onlyMoved ? editor3DPlaceInstance(made, object) : editor3DMakeInstance(object);
|
|
35841
36943
|
}
|
|
36944
|
+
editor3DShadowLight();
|
|
36945
|
+
}
|
|
36946
|
+
|
|
36947
|
+
// the light that casts the shadows is the level's last Light that asks to, as it is after a load, whatever order
|
|
36948
|
+
// the edits made them in; a light of the game's own is left alone while no Light of the level asks
|
|
36949
|
+
function editor3DShadowLight()
|
|
36950
|
+
{
|
|
36951
|
+
if (!render3D) return;
|
|
36952
|
+
let light;
|
|
36953
|
+
for (const object of editor3DObjects())
|
|
36954
|
+
{
|
|
36955
|
+
const made = editor3DInstances.get(object.id);
|
|
36956
|
+
if (object.type === 'Light' && object.properties?.shadows === true && made instanceof Light3D && !made.destroyed)
|
|
36957
|
+
light = made;
|
|
36958
|
+
}
|
|
36959
|
+
const now = render3D.shadowLight;
|
|
36960
|
+
if (light || !now || now.destroyed || [...editor3DInstances.values()].some((made)=> made === now))
|
|
36961
|
+
render3D.shadowLight = light;
|
|
35842
36962
|
}
|
|
35843
36963
|
|
|
35844
36964
|
// change the level's objects as part of the edit being made: change edits a copy of the list; the changes until
|
|
@@ -35867,8 +36987,30 @@ function editor3DSetPart(name, part)
|
|
|
35867
36987
|
}
|
|
35868
36988
|
else if (name === 'voxels')
|
|
35869
36989
|
editor3DVoxelShow();
|
|
35870
|
-
else
|
|
36990
|
+
else if (name === 'terrain')
|
|
35871
36991
|
editor3DTerrainShow();
|
|
36992
|
+
else
|
|
36993
|
+
editor3DPrefabsShow();
|
|
36994
|
+
}
|
|
36995
|
+
|
|
36996
|
+
// bring the prefab types in line with the level's own prefabs, and make every instance again: a prefab the level
|
|
36997
|
+
// no longer has is no longer a type, one it has is added as level3DLoad adds it, and what a prefab makes is made
|
|
36998
|
+
// when its instance is, so each instance is made again
|
|
36999
|
+
function editor3DPrefabsShow()
|
|
37000
|
+
{
|
|
37001
|
+
if (editor3DPrefabLevels.has(editor3DLevel)) return; // inside a prefab, the prefabs are the level's
|
|
37002
|
+
const prefabs = editor3DLevelPart('prefabs') ?? {};
|
|
37003
|
+
for (const [name, prefab] of level3DPrefabs)
|
|
37004
|
+
{
|
|
37005
|
+
if (!prefab.fromLevel || prefabs[name]) continue;
|
|
37006
|
+
level3DPrefabs.delete(name);
|
|
37007
|
+
level3DTypes.delete(name);
|
|
37008
|
+
}
|
|
37009
|
+
level3DPrefabsAdd(prefabs);
|
|
37010
|
+
// every object of a prefab's type, one whose type was not there when the objects were set too
|
|
37011
|
+
for (const object of editor3DObjects())
|
|
37012
|
+
level3DPrefabs.has(object.type) && editor3DMakeInstance(object);
|
|
37013
|
+
editor3DShadowLight();
|
|
35872
37014
|
}
|
|
35873
37015
|
|
|
35874
37016
|
// make the level's parts these, the ones that differ
|
|
@@ -35878,6 +37020,11 @@ function editor3DSetParts(parts)
|
|
|
35878
37020
|
editor3DSame(parts[name], editor3DLevelPart(name)) || editor3DSetPart(name, parts[name]);
|
|
35879
37021
|
}
|
|
35880
37022
|
|
|
37023
|
+
// the stroke being made, started here with the level as it is when there is none: called before the level changes
|
|
37024
|
+
/** @return {{before: Array<Object>, parts: Object, painted?: boolean, sculpted?: boolean}} */
|
|
37025
|
+
function editor3DStrokeBegin()
|
|
37026
|
+
{ return editor3DStroke ||= {before: editor3DCopy(editor3DObjects()), parts: editor3DLevelParts()}; }
|
|
37027
|
+
|
|
35881
37028
|
// change a part of the level as part of the edit being made, as editor3DChange does its objects: change is given
|
|
35882
37029
|
// a copy of the part, undefined when the level has none, and returns the new one, or undefined for none; false
|
|
35883
37030
|
// when nothing changed
|
|
@@ -35886,7 +37033,7 @@ function editor3DChangePart(name, change)
|
|
|
35886
37033
|
if (!editor3DLevel || editor3DRecords.get(editor3DLevel)?.pending) return false; // its autosave waits first
|
|
35887
37034
|
const parts = editor3DLevelParts(), after = change(parts[name] && editor3DCopy(parts[name]));
|
|
35888
37035
|
if (editor3DSame(parts[name], after)) return false;
|
|
35889
|
-
|
|
37036
|
+
editor3DStrokeBegin();
|
|
35890
37037
|
editor3DSetPart(name, after);
|
|
35891
37038
|
return true;
|
|
35892
37039
|
}
|
|
@@ -35963,8 +37110,7 @@ function editor3DVoxelSet(cell, type)
|
|
|
35963
37110
|
const map = editor3DVoxelMap();
|
|
35964
37111
|
if (!map || editor3DRecords.get(editor3DLevel)?.pending) return false; // its autosave waits first
|
|
35965
37112
|
if (!editor3DVoxelInside(map, cell) || map.getVoxel(cell) === type) return false;
|
|
35966
|
-
|
|
35967
|
-
editor3DStroke.painted = true;
|
|
37113
|
+
editor3DStrokeBegin().painted = true;
|
|
35968
37114
|
map.setVoxel(cell, type);
|
|
35969
37115
|
return true;
|
|
35970
37116
|
}
|
|
@@ -35986,8 +37132,85 @@ function editor3DVoxelDragCell(drag, ray)
|
|
|
35986
37132
|
// the level's terrain: the level's terrain block is the source of truth, the game's HeightMap follows it;
|
|
35987
37133
|
// sculpting changes the game's map as it goes, and the stroke's end writes its heights into the level
|
|
35988
37134
|
|
|
35989
|
-
// the brush: its size across in world units,
|
|
35990
|
-
|
|
37135
|
+
// the brush: its size across in world units, how strong it is, 0 to 1, which brush it is, sculpt, flatten or
|
|
37136
|
+
// paint, and the color it paints
|
|
37137
|
+
const editor3DTerrainBrush = {size: 8, strength: .5, mode: 'sculpt', color: '#8a6a4a'};
|
|
37138
|
+
const editor3DTerrainBrushes = ['sculpt', 'flatten', 'paint'];
|
|
37139
|
+
|
|
37140
|
+
// the paint of the game's terrain map as it is being edited, {colors, cells} as level3DTerrainPaint gives it:
|
|
37141
|
+
// from the level, and ahead of it while a stroke paints
|
|
37142
|
+
const editor3DTerrainPaints = new WeakMap;
|
|
37143
|
+
function editor3DTerrainPaint(map)
|
|
37144
|
+
{
|
|
37145
|
+
let paint = editor3DTerrainPaints.get(map);
|
|
37146
|
+
if (!paint)
|
|
37147
|
+
{
|
|
37148
|
+
const samples = map.rows * map.columns;
|
|
37149
|
+
paint = level3DTerrainPaint(editor3DLevelPart('terrain'), samples) || {colors: [], cells: new Uint8Array(samples)};
|
|
37150
|
+
editor3DTerrainPaints.set(map, paint);
|
|
37151
|
+
}
|
|
37152
|
+
return paint;
|
|
37153
|
+
}
|
|
37154
|
+
|
|
37155
|
+
// the next brush of the Terrain tool
|
|
37156
|
+
function editor3DTerrainNextBrush()
|
|
37157
|
+
{
|
|
37158
|
+
const brushes = editor3DTerrainBrushes;
|
|
37159
|
+
editor3DTerrainBrush.mode = brushes[(brushes.indexOf(editor3DTerrainBrush.mode) + 1) % brushes.length];
|
|
37160
|
+
}
|
|
37161
|
+
|
|
37162
|
+
// the drag a press on the ground starts, by the brush and the keys held: sculpt raises, Shift lowers; flatten
|
|
37163
|
+
// brings the ground to the height pressed; paint colors it, Shift takes the paint off; Ctrl smooths with the two
|
|
37164
|
+
// that shape the ground
|
|
37165
|
+
function editor3DTerrainPress(point, shift, ctrl)
|
|
37166
|
+
{
|
|
37167
|
+
const map = editor3DTerrainMap(), brush = editor3DTerrainBrush.mode;
|
|
37168
|
+
if (!map) return;
|
|
37169
|
+
if (brush === 'paint')
|
|
37170
|
+
return {kind: 'terrain', mode: shift ? 'erase' : 'paint'};
|
|
37171
|
+
if (ctrl)
|
|
37172
|
+
return {kind: 'terrain', mode: 'smooth'};
|
|
37173
|
+
if (brush === 'flatten')
|
|
37174
|
+
return {kind: 'terrain', mode: 'flatten', level: clamp((point.y - map.pos3D.y) / map.height)};
|
|
37175
|
+
return {kind: 'terrain', mode: shift ? 'lower' : 'raise'};
|
|
37176
|
+
}
|
|
37177
|
+
|
|
37178
|
+
// the mouse held on the ground with a terrain drag
|
|
37179
|
+
function editor3DTerrainDragTo(drag, point)
|
|
37180
|
+
{
|
|
37181
|
+
return drag.mode === 'paint' || drag.mode === 'erase' ? editor3DTerrainPaintAt(point, drag.mode === 'erase') :
|
|
37182
|
+
editor3DTerrainSculpt(point, drag.mode, editor3DSeconds, drag.level);
|
|
37183
|
+
}
|
|
37184
|
+
|
|
37185
|
+
// paint the ground around a point with the brush's color, or take its paint off, as part of the edit being made:
|
|
37186
|
+
// every sample under the brush, in the game's map now and in the level when the stroke ends; false when nothing
|
|
37187
|
+
// changed, or when the terrain has all the colors it can keep, 255
|
|
37188
|
+
function editor3DTerrainPaintAt(point, erase=false)
|
|
37189
|
+
{
|
|
37190
|
+
const map = editor3DTerrainMap();
|
|
37191
|
+
if (!map || editor3DRecords.get(editor3DLevel)?.pending) return false; // its autosave waits first
|
|
37192
|
+
const paint = editor3DTerrainPaint(map), hex = editor3DInputColor(editor3DTerrainBrush.color);
|
|
37193
|
+
let color = erase ? 0 : paint.colors.indexOf(hex) + 1;
|
|
37194
|
+
if (!erase && !color && paint.colors.length >= 255) return false;
|
|
37195
|
+
const radius = editor3DTerrainBrush.size / 2, rows = map.rows, columns = map.columns;
|
|
37196
|
+
const stepX = map.mapSize.x / (columns - 1), stepZ = map.mapSize.y / (rows - 1);
|
|
37197
|
+
const x0 = map.pos3D.x - map.mapSize.x / 2, z0 = map.pos3D.z - map.mapSize.y / 2;
|
|
37198
|
+
let changed = false;
|
|
37199
|
+
for (let r = 0; r < rows; ++r)
|
|
37200
|
+
for (let c = 0; c < columns; ++c)
|
|
37201
|
+
{
|
|
37202
|
+
if (hypot(x0 + c * stepX - point.x, z0 + r * stepZ - point.z) >= radius) continue;
|
|
37203
|
+
color ||= erase ? 0 : paint.colors.push(hex); // a new color joins the list with the first sample it paints
|
|
37204
|
+
if (paint.cells[r * columns + c] === color) continue;
|
|
37205
|
+
paint.cells[r * columns + c] = color;
|
|
37206
|
+
changed = true;
|
|
37207
|
+
}
|
|
37208
|
+
if (!changed) return false;
|
|
37209
|
+
editor3DStrokeBegin().sculpted = true;
|
|
37210
|
+
level3DTerrainSetColors(map, editor3DLevelPart('terrain'), paint);
|
|
37211
|
+
map.rebuild();
|
|
37212
|
+
return true;
|
|
37213
|
+
}
|
|
35991
37214
|
|
|
35992
37215
|
// the game's map of the level's terrain, undefined when the level has none
|
|
35993
37216
|
function editor3DTerrainMap()
|
|
@@ -36016,15 +37239,30 @@ function editor3DTerrainShow()
|
|
|
36016
37239
|
return;
|
|
36017
37240
|
}
|
|
36018
37241
|
map.heights = shape.heights;
|
|
36019
|
-
|
|
37242
|
+
editor3DTerrainPaints.delete(map); // as the level has it
|
|
37243
|
+
level3DTerrainSetColors(map, terrain);
|
|
36020
37244
|
map.rebuild();
|
|
36021
37245
|
}
|
|
36022
37246
|
|
|
36023
|
-
// write the game's map into the level, as a stroke of sculpting ends, its heights to a thousandth
|
|
37247
|
+
// write the game's map into the level, as a stroke of sculpting ends, its heights to a thousandth, and its paint:
|
|
37248
|
+
// the colors still in use and runs of a count and a color along the rows, or no paint when none is left
|
|
36024
37249
|
function editor3DTerrainStore()
|
|
36025
37250
|
{
|
|
36026
37251
|
const map = editor3DTerrainMap(), terrain = editor3DLevelPart('terrain');
|
|
36027
|
-
|
|
37252
|
+
if (!map || !terrain) return;
|
|
37253
|
+
terrain.heights = map.heights.map((row)=> row.map((v)=> round(v * 1e3) / 1e3));
|
|
37254
|
+
const paint = editor3DTerrainPaints.get(map);
|
|
37255
|
+
if (!paint) return; // nothing painted since the level's was read
|
|
37256
|
+
const used = paint.colors.map((hex, i)=> paint.cells.includes(i + 1));
|
|
37257
|
+
const colors = paint.colors.filter((hex, i)=> used[i]), cells = [];
|
|
37258
|
+
const index = paint.colors.map((hex, i)=> used[i] ? colors.indexOf(hex) + 1 : 0);
|
|
37259
|
+
for (const v of paint.cells)
|
|
37260
|
+
{
|
|
37261
|
+
const color = v && index[v - 1];
|
|
37262
|
+
cells[cells.length - 1] === color ? ++cells[cells.length - 2] : cells.push(1, color);
|
|
37263
|
+
}
|
|
37264
|
+
colors.length ? terrain.paint = {colors, cells} : delete terrain.paint;
|
|
37265
|
+
editor3DTerrainPaints.delete(map); // read from the level again, with its colors as they are numbered now
|
|
36028
37266
|
}
|
|
36029
37267
|
|
|
36030
37268
|
// give the level a flat terrain, as one undo: its size in the world, how many cells a side, and how tall a full
|
|
@@ -36048,9 +37286,9 @@ function editor3DTerrainRemove()
|
|
|
36048
37286
|
}
|
|
36049
37287
|
|
|
36050
37288
|
// sculpt the ground around a point for some seconds, as part of the edit being made: raise, lower or smooth it
|
|
36051
|
-
// under the brush, a soft circle, most in its middle and nothing at its edge
|
|
36052
|
-
// when the stroke ends; false when nothing changed
|
|
36053
|
-
function editor3DTerrainSculpt(point, mode, seconds)
|
|
37289
|
+
// under the brush, a soft circle, most in its middle and nothing at its edge, or flatten it toward a height, level,
|
|
37290
|
+
// 0 to 1; in the game's map now, in the level when the stroke ends; false when nothing changed
|
|
37291
|
+
function editor3DTerrainSculpt(point, mode, seconds, level=0)
|
|
36054
37292
|
{
|
|
36055
37293
|
const map = editor3DTerrainMap();
|
|
36056
37294
|
if (!map || editor3DRecords.get(editor3DLevel)?.pending) return false; // its autosave waits first
|
|
@@ -36061,7 +37299,7 @@ function editor3DTerrainSculpt(point, mode, seconds)
|
|
|
36061
37299
|
const c0 = max(0, ceil((point.x - radius - x0) / stepX)), c1 = min(columns - 1, floor((point.x + radius - x0) / stepX));
|
|
36062
37300
|
const r0 = max(0, ceil((point.z - radius - z0) / stepZ)), r1 = min(rows - 1, floor((point.z + radius - z0) / stepZ));
|
|
36063
37301
|
// smoothing reads the ground as it was, so the order the samples are done in does not show
|
|
36064
|
-
const old = mode === 'smooth' ? heights.map((row)=> row.slice()) : heights;
|
|
37302
|
+
const old = mode === 'smooth' ? heights.map((row, r)=> r >= r0 - 1 && r <= r1 + 1 ? row.slice() : row) : heights;
|
|
36065
37303
|
const rate = strength * 10 * seconds; // world units of height at full strength, 10 a second
|
|
36066
37304
|
let changed = false;
|
|
36067
37305
|
for (let r = r0; r <= r1; ++r)
|
|
@@ -36079,6 +37317,8 @@ function editor3DTerrainSculpt(point, mode, seconds)
|
|
|
36079
37317
|
old[r + dr]?.[c + dc] !== undefined && (sum += old[r + dr][c + dc], ++count);
|
|
36080
37318
|
v = h + (sum / count - h) * min(1, rate * falloff);
|
|
36081
37319
|
}
|
|
37320
|
+
else if (mode === 'flatten')
|
|
37321
|
+
v = h + (level - h) * min(1, rate * falloff);
|
|
36082
37322
|
else
|
|
36083
37323
|
v = clamp(h + (mode === 'lower' ? -1 : 1) * rate * falloff / map.height);
|
|
36084
37324
|
if (v === h) continue;
|
|
@@ -36086,8 +37326,7 @@ function editor3DTerrainSculpt(point, mode, seconds)
|
|
|
36086
37326
|
changed = true;
|
|
36087
37327
|
}
|
|
36088
37328
|
if (!changed) return false;
|
|
36089
|
-
|
|
36090
|
-
editor3DStroke.sculpted = true;
|
|
37329
|
+
editor3DStrokeBegin().sculpted = true;
|
|
36091
37330
|
map.rebuild();
|
|
36092
37331
|
return true;
|
|
36093
37332
|
}
|
|
@@ -36108,16 +37347,81 @@ function editor3DVoxelPress(ray, mode)
|
|
|
36108
37347
|
{
|
|
36109
37348
|
const target = editor3DVoxelTarget(ray, mode);
|
|
36110
37349
|
if (!target) return;
|
|
36111
|
-
const {cell, axis, plane} = target;
|
|
37350
|
+
const {cell, axis, plane, side} = target;
|
|
37351
|
+
if (editor3DBlockBox)
|
|
37352
|
+
{
|
|
37353
|
+
// a box grows out from the face when placing, and into the blocks when removing or repainting
|
|
37354
|
+
const drag = {kind: 'blocks', mode, axis, plane, layer: cell[axis], last: cell, start: cell,
|
|
37355
|
+
grow: mode === 'place' ? side : -side, height: max(1, floor(editor3DBlockBoxHeight)),
|
|
37356
|
+
changed: new Map, region: undefined};
|
|
37357
|
+
editor3DVoxelBoxTo(drag, cell);
|
|
37358
|
+
return drag;
|
|
37359
|
+
}
|
|
36112
37360
|
editor3DVoxelApply(cell, mode);
|
|
36113
37361
|
return {kind: 'blocks', mode, axis, plane, layer: cell[axis], last: cell};
|
|
36114
37362
|
}
|
|
36115
37363
|
|
|
37364
|
+
// a box drag goes on: what it changed so far is put back, then every cell of the box from where it started to a
|
|
37365
|
+
// cell of the same layer is changed, as many layers as its height, the part of it inside the map
|
|
37366
|
+
function editor3DVoxelBoxTo(drag, cell)
|
|
37367
|
+
{
|
|
37368
|
+
const map = editor3DVoxelMap();
|
|
37369
|
+
if (!map) return;
|
|
37370
|
+
for (const [at, type] of drag.changed.values())
|
|
37371
|
+
map.setVoxel(at, type);
|
|
37372
|
+
drag.changed.clear();
|
|
37373
|
+
const lo = vec3(min(drag.start.x, cell.x), min(drag.start.y, cell.y), min(drag.start.z, cell.z));
|
|
37374
|
+
const hi = vec3(max(drag.start.x, cell.x), max(drag.start.y, cell.y), max(drag.start.z, cell.z));
|
|
37375
|
+
const far = drag.layer + drag.grow * (drag.height - 1), axis = drag.axis, s = map.mapSize;
|
|
37376
|
+
lo[axis] = clamp(min(drag.layer, far), 0, s[axis] - 1);
|
|
37377
|
+
hi[axis] = clamp(max(drag.layer, far), 0, s[axis] - 1);
|
|
37378
|
+
for (let z = lo.z; z <= hi.z; ++z)
|
|
37379
|
+
for (let y = lo.y; y <= hi.y; ++y)
|
|
37380
|
+
for (let x = lo.x; x <= hi.x; ++x)
|
|
37381
|
+
{
|
|
37382
|
+
const at = vec3(x, y, z), type = map.getVoxel(at);
|
|
37383
|
+
editor3DVoxelApply(at, drag.mode) && drag.changed.set(x + s.x * (y + s.y * z), [at, type]);
|
|
37384
|
+
}
|
|
37385
|
+
drag.region = {lo, hi};
|
|
37386
|
+
drag.last = cell;
|
|
37387
|
+
}
|
|
37388
|
+
|
|
37389
|
+
// change the size of the level's map in cells, as one undo: it grows and shrinks about its middle across, and at
|
|
37390
|
+
// its top, and its blocks stay where they are in the world, the ones outside the new size gone
|
|
37391
|
+
function editor3DVoxelResize(size)
|
|
37392
|
+
{
|
|
37393
|
+
const changed = editor3DChangePart('voxels', (voxels)=>
|
|
37394
|
+
{
|
|
37395
|
+
const shape = level3DVoxelsShape(voxels);
|
|
37396
|
+
const to = vec3(floor(size.x), floor(size.y), floor(size.z));
|
|
37397
|
+
if (!shape || to.x < 1 || to.y < 1 || to.z < 1 || to.x * to.y * to.z > LEVEL3D_VOXEL_CELLS) return voxels;
|
|
37398
|
+
const from = shape.size, old = new Uint8Array(from.x * from.y * from.z);
|
|
37399
|
+
level3DVoxelsDecode(voxels.blocks, old);
|
|
37400
|
+
// how far each block moves in cells, so the corner moves the other way and the blocks stay put
|
|
37401
|
+
const dx = floor((to.x - from.x) / 2), dz = floor((to.z - from.z) / 2);
|
|
37402
|
+
const data = new Uint8Array(to.x * to.y * to.z);
|
|
37403
|
+
for (let z = 0; z < from.z; ++z)
|
|
37404
|
+
for (let y = 0; y < min(from.y, to.y); ++y)
|
|
37405
|
+
for (let x = 0; x < from.x; ++x)
|
|
37406
|
+
{
|
|
37407
|
+
const nx = x + dx, nz = z + dz;
|
|
37408
|
+
if (nx >= 0 && nz >= 0 && nx < to.x && nz < to.z)
|
|
37409
|
+
data[nx + to.x * (y + to.y * nz)] = old[x + from.x * (y + from.y * z)];
|
|
37410
|
+
}
|
|
37411
|
+
return {...voxels, pos: [shape.pos.x - dx, shape.pos.y, shape.pos.z - dz], size: [to.x, to.y, to.z],
|
|
37412
|
+
blocks: level3DVoxelsEncode(data)};
|
|
37413
|
+
});
|
|
37414
|
+
editor3DStrokeEnd();
|
|
37415
|
+
return changed;
|
|
37416
|
+
}
|
|
37417
|
+
|
|
36116
37418
|
// a drag of the Blocks tool goes on: every cell of the layer from the last one to the one under the ray
|
|
36117
37419
|
function editor3DVoxelDragTo(drag, ray)
|
|
36118
37420
|
{
|
|
36119
37421
|
const cell = editor3DVoxelDragCell(drag, ray), last = drag.last;
|
|
36120
37422
|
if (!cell) return;
|
|
37423
|
+
if (drag.changed)
|
|
37424
|
+
return editor3DVoxelBoxTo(drag, cell);
|
|
36121
37425
|
const d = cell.subtract(last), steps = max(abs(d.x), abs(d.y), abs(d.z));
|
|
36122
37426
|
for (let i = 1; i <= steps; ++i)
|
|
36123
37427
|
editor3DVoxelApply(vec3(round(last.x + d.x * i / steps), round(last.y + d.y * i / steps),
|
|
@@ -36144,12 +37448,12 @@ function editor3DVoxelTarget(ray, mode)
|
|
|
36144
37448
|
if (!hit)
|
|
36145
37449
|
{
|
|
36146
37450
|
const cell = place ? editor3DVoxelDragCell({axis: 'y', plane: 0, layer: 0}, ray) : undefined;
|
|
36147
|
-
return cell && {cell, axis: 'y', plane: 0};
|
|
37451
|
+
return cell && {cell, axis: 'y', plane: 0, side: 1};
|
|
36148
37452
|
}
|
|
36149
37453
|
const n = hit.normal, axis = n.x ? 'x' : n.y ? 'y' : 'z', cell = place ? hit.cell.add(n) : hit.cell;
|
|
36150
37454
|
if (!editor3DVoxelInside(map, cell)) return;
|
|
36151
|
-
// the face between the block hit and the cell in front of it
|
|
36152
|
-
return {cell, axis, plane: cell[axis] + ((n[axis] > 0) === place ? 0 : 1)};
|
|
37455
|
+
// the face between the block hit and the cell in front of it, and side, the way that face looks along the axis
|
|
37456
|
+
return {cell, axis, plane: cell[axis] + ((n[axis] > 0) === place ? 0 : 1), side: n[axis]};
|
|
36153
37457
|
}
|
|
36154
37458
|
|
|
36155
37459
|
// end the edit being made: one undo, and the autosave
|
|
@@ -36223,6 +37527,7 @@ function editor3DSetProperty(object, name, value, defaultValue)
|
|
|
36223
37527
|
// add an object of a type at a position, selected; its id, or undefined when the level can not be edited
|
|
36224
37528
|
function editor3DPlace(type, pos)
|
|
36225
37529
|
{
|
|
37530
|
+
if (editor3DPrefabSelf(type)) return; // it would hold itself
|
|
36226
37531
|
let id;
|
|
36227
37532
|
const placed = editor3DChange((list)=>
|
|
36228
37533
|
{
|
|
@@ -36236,6 +37541,302 @@ function editor3DPlace(type, pos)
|
|
|
36236
37541
|
return id;
|
|
36237
37542
|
}
|
|
36238
37543
|
|
|
37544
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
37545
|
+
// prefabs: a selection made one, an instance made objects again, and a prefab's own settings
|
|
37546
|
+
|
|
37547
|
+
// what the panel says about the last prefab action that was refused
|
|
37548
|
+
let editor3DPrefabMessage = '';
|
|
37549
|
+
|
|
37550
|
+
// the box around an object in the world, turned and sized as it is, as its low and high corners
|
|
37551
|
+
function editor3DWorldBox(object, low=vec3(Infinity), high=vec3(-Infinity))
|
|
37552
|
+
{
|
|
37553
|
+
const matrix = editor3DBoxMatrix(object);
|
|
37554
|
+
for (let i = 8; i--;)
|
|
37555
|
+
{
|
|
37556
|
+
const p = matrix.transformPoint(vec3(i & 1 ? .5 : -.5, i & 2 ? .5 : -.5, i & 4 ? .5 : -.5));
|
|
37557
|
+
for (const k of ['x', 'y', 'z'])
|
|
37558
|
+
low[k] = min(low[k], p[k]), high[k] = max(high[k], p[k]);
|
|
37559
|
+
}
|
|
37560
|
+
return {low, high};
|
|
37561
|
+
}
|
|
37562
|
+
|
|
37563
|
+
// is a type the prefab being edited, or one that holds it: a prefab that is open, or one the game made of the
|
|
37564
|
+
// level being edited itself, as a prefab maker does; placed there it would hold itself
|
|
37565
|
+
function editor3DPrefabSelf(type)
|
|
37566
|
+
{
|
|
37567
|
+
const names = editor3DPrefabStack.map((frame)=> frame.name);
|
|
37568
|
+
for (const [name, prefab] of level3DPrefabs)
|
|
37569
|
+
prefab.source === editor3DLevel && names.push(name);
|
|
37570
|
+
return names.some((name)=> editor3DPrefabHolds(type, name));
|
|
37571
|
+
}
|
|
37572
|
+
|
|
37573
|
+
// the types the Place list has, the plain ones and then the prefabs
|
|
37574
|
+
function editor3DPlaceNames()
|
|
37575
|
+
{
|
|
37576
|
+
const all = [...level3DTypes.keys()].filter((name)=> !editor3DPrefabSelf(name));
|
|
37577
|
+
return [...all.filter((name)=> !level3DPrefabs.has(name)), ...all.filter((name)=> level3DPrefabs.has(name))];
|
|
37578
|
+
}
|
|
37579
|
+
|
|
37580
|
+
// does a type hold a prefab, itself or through the prefabs it holds
|
|
37581
|
+
function editor3DPrefabHolds(type, name, depth=0)
|
|
37582
|
+
{
|
|
37583
|
+
return type === name || depth < 8 &&
|
|
37584
|
+
!!level3DPrefabs.get(type)?.objects.some((o)=> editor3DPrefabHolds(o.type, name, depth + 1));
|
|
37585
|
+
}
|
|
37586
|
+
|
|
37587
|
+
// make the selected objects a prefab of the level's own, about the bottom centre of their box, and put one
|
|
37588
|
+
// instance of it where they were, as one undo; a name the level's prefabs have replaces that prefab, and every
|
|
37589
|
+
// instance of it changes; true, or why not, which the panel shows
|
|
37590
|
+
function editor3DMakePrefab(name='')
|
|
37591
|
+
{
|
|
37592
|
+
const refuse = (why)=> editor3DPrefabMessage = why;
|
|
37593
|
+
const selected = editor3DSelected();
|
|
37594
|
+
if (editor3DPrefabStack.length) return refuse('Go back to the level to make a prefab');
|
|
37595
|
+
if (!selected.length) return refuse('Select the objects to make a prefab of');
|
|
37596
|
+
if (!editor3DLevel || editor3DRecords.get(editor3DLevel)?.pending) return refuse('The level can not be edited now');
|
|
37597
|
+
name = String(name).trim();
|
|
37598
|
+
for (let i = 1; !name; ++i)
|
|
37599
|
+
level3DTypes.has('Prefab ' + i) || (name = 'Prefab ' + i);
|
|
37600
|
+
const known = level3DPrefabs.get(name);
|
|
37601
|
+
if (known ? !known.fromLevel : level3DTypes.has(name))
|
|
37602
|
+
return refuse(name + ' is a type the game added, pick another name');
|
|
37603
|
+
if (selected.some((o)=> editor3DPrefabHolds(o.type, name)))
|
|
37604
|
+
return refuse(name + ' can not hold itself');
|
|
37605
|
+
|
|
37606
|
+
// its origin is the bottom centre of the selection, so an instance stands on the ground
|
|
37607
|
+
const low = vec3(Infinity), high = vec3(-Infinity);
|
|
37608
|
+
for (const object of selected)
|
|
37609
|
+
editor3DWorldBox(object, low, high);
|
|
37610
|
+
const origin = vec3(editor3DRound((low.x + high.x) / 2), editor3DRound(low.y), editor3DRound((low.z + high.z) / 2));
|
|
37611
|
+
const objects = selected.map((object, i)=>
|
|
37612
|
+
{
|
|
37613
|
+
const part = {...editor3DCopy(object), id: i + 1};
|
|
37614
|
+
editor3DSetTransform(part, editor3DPos(object).subtract(origin));
|
|
37615
|
+
return part;
|
|
37616
|
+
});
|
|
37617
|
+
editor3DStrokeEnd();
|
|
37618
|
+
const id = editor3DObjects().reduce((next, o)=> max(next, o.id + 1), 1);
|
|
37619
|
+
const ids = new Set(selected.map((o)=> o.id));
|
|
37620
|
+
// the prefab first, so its type is there when the instance is made
|
|
37621
|
+
editor3DChangePart('prefabs', (prefabs={})=>
|
|
37622
|
+
({...prefabs, [name]: prefabs[name]?.attached ? {attached: true, objects} : {objects}}));
|
|
37623
|
+
editor3DChange((list)=>
|
|
37624
|
+
{
|
|
37625
|
+
for (let i = list.length; i--;)
|
|
37626
|
+
ids.has(list[i].id) && list.splice(i, 1);
|
|
37627
|
+
const instance = {id, type: name};
|
|
37628
|
+
editor3DSetTransform(instance, origin);
|
|
37629
|
+
list.push(instance);
|
|
37630
|
+
});
|
|
37631
|
+
editor3DStrokeEnd();
|
|
37632
|
+
editor3DSelection.clear();
|
|
37633
|
+
editor3DSelection.add(id);
|
|
37634
|
+
editor3DPrefabMessage = '';
|
|
37635
|
+
return true;
|
|
37636
|
+
}
|
|
37637
|
+
|
|
37638
|
+
// add a prefab from the text of a file the editor saved, a level file, to the level's own prefabs under a name,
|
|
37639
|
+
// as one undo, and pick it to place; a name the level's prefabs have is replaced, as Make prefab does; true, or
|
|
37640
|
+
// why not, which the panel shows
|
|
37641
|
+
function editor3DPrefabLoad(name, text)
|
|
37642
|
+
{
|
|
37643
|
+
const refuse = (why)=> editor3DPrefabMessage = why;
|
|
37644
|
+
if (editor3DPrefabStack.length) return refuse('Go back to the level to load a prefab');
|
|
37645
|
+
if (!editor3DLevel || editor3DRecords.get(editor3DLevel)?.pending) return refuse('The level can not be edited now');
|
|
37646
|
+
name = String(name).trim();
|
|
37647
|
+
if (!name) return refuse('The prefab needs a name');
|
|
37648
|
+
let file;
|
|
37649
|
+
try { file = JSON.parse(text); }
|
|
37650
|
+
catch { return refuse(name + ' is not a JSON file'); }
|
|
37651
|
+
if (!file || typeof file !== 'object' || !isArray(file.objects))
|
|
37652
|
+
return refuse(name + ' is not a prefab, it has no objects');
|
|
37653
|
+
const known = level3DPrefabs.get(name);
|
|
37654
|
+
if (known ? !known.fromLevel : level3DTypes.has(name))
|
|
37655
|
+
return refuse(name + ' is a type the game added, rename the file');
|
|
37656
|
+
const objects = file.objects.filter((o)=> o && typeof o === 'object');
|
|
37657
|
+
if (objects.some((o)=> editor3DPrefabHolds(o.type, name)))
|
|
37658
|
+
return refuse(name + ' can not hold itself');
|
|
37659
|
+
editor3DStrokeEnd();
|
|
37660
|
+
editor3DChangePart('prefabs', (prefabs={})=> ({...prefabs, [name]: file.attached ? {attached: true, objects} : {objects}}));
|
|
37661
|
+
editor3DStrokeEnd();
|
|
37662
|
+
editor3DPickTool(editor3DTool); // a tool of the game's own is put down
|
|
37663
|
+
editor3DBrush = name;
|
|
37664
|
+
editor3DPrefabMessage = '';
|
|
37665
|
+
return true;
|
|
37666
|
+
}
|
|
37667
|
+
|
|
37668
|
+
// pick a prefab's file and load it, by its file name
|
|
37669
|
+
function editor3DPrefabPickFile()
|
|
37670
|
+
{
|
|
37671
|
+
const input = document.createElement('input');
|
|
37672
|
+
input.type = 'file';
|
|
37673
|
+
input.accept = '.json,application/json';
|
|
37674
|
+
input.onchange = async ()=>
|
|
37675
|
+
{
|
|
37676
|
+
const file = input.files?.[0];
|
|
37677
|
+
file && editor3DPrefabLoad(file.name.replace(/\.json$/i, ''), await file.text());
|
|
37678
|
+
};
|
|
37679
|
+
input.click();
|
|
37680
|
+
}
|
|
37681
|
+
|
|
37682
|
+
// turn the selected instances of prefabs into the objects they are made of, where they are, as one undo; false
|
|
37683
|
+
// with none selected
|
|
37684
|
+
function editor3DUnpack()
|
|
37685
|
+
{
|
|
37686
|
+
const instances = editor3DSelected().filter((o)=> editor3DInstances.get(o.id) instanceof Prefab3D);
|
|
37687
|
+
if (!instances.length) return false;
|
|
37688
|
+
editor3DStrokeEnd();
|
|
37689
|
+
const added = [];
|
|
37690
|
+
const changed = editor3DChange((list)=>
|
|
37691
|
+
{
|
|
37692
|
+
let id = list.reduce((next, o)=> max(next, o.id + 1), 1);
|
|
37693
|
+
for (const instance of instances)
|
|
37694
|
+
{
|
|
37695
|
+
const made = editor3DInstances.get(instance.id);
|
|
37696
|
+
// by the level's own place of it, its origin, which is not where an attached one's handle is
|
|
37697
|
+
const origin = {pos3D: editor3DPos(instance), rotation3D: editor3DRotation(instance).scale(PI / 180),
|
|
37698
|
+
scale3D: editor3DScale(instance)};
|
|
37699
|
+
for (const part of level3DPrefabs.get(made.prefabName)?.objects ?? [])
|
|
37700
|
+
{
|
|
37701
|
+
const at = level3DPrefabPartTransform(origin, part), object = {...editor3DCopy(part), id: id++};
|
|
37702
|
+
editor3DSetTransform(object, at.pos, at.rotation.scale(180 / PI), at.scale);
|
|
37703
|
+
list.push(object);
|
|
37704
|
+
added.push(object.id);
|
|
37705
|
+
}
|
|
37706
|
+
list.splice(list.findIndex((o)=> o.id === instance.id), 1);
|
|
37707
|
+
}
|
|
37708
|
+
});
|
|
37709
|
+
editor3DStrokeEnd();
|
|
37710
|
+
if (!changed) return false;
|
|
37711
|
+
editor3DSelection.clear();
|
|
37712
|
+
for (const id of added)
|
|
37713
|
+
editor3DSelection.add(id);
|
|
37714
|
+
return true;
|
|
37715
|
+
}
|
|
37716
|
+
|
|
37717
|
+
// make a prefab of the level's own attached, its parts the children of each instance, or not, as one undo
|
|
37718
|
+
function editor3DPrefabSetAttached(name, attached)
|
|
37719
|
+
{
|
|
37720
|
+
if (editor3DPrefabStack.length) return false; // a prefab is the level's, set from the level
|
|
37721
|
+
editor3DStrokeEnd();
|
|
37722
|
+
const changed = editor3DChangePart('prefabs', (prefabs)=>
|
|
37723
|
+
{
|
|
37724
|
+
if (!prefabs?.[name]) return prefabs;
|
|
37725
|
+
const {attached: was, ...rest} = prefabs[name];
|
|
37726
|
+
return {...prefabs, [name]: attached ? {attached: true, ...rest} : rest};
|
|
37727
|
+
});
|
|
37728
|
+
editor3DStrokeEnd();
|
|
37729
|
+
return changed;
|
|
37730
|
+
}
|
|
37731
|
+
|
|
37732
|
+
// a prefab as its own file has it, a level file of its objects, to load with level3DLoadPrefab
|
|
37733
|
+
function editor3DPrefabJSON(name)
|
|
37734
|
+
{
|
|
37735
|
+
const prefab = level3DPrefabs.get(name);
|
|
37736
|
+
if (!prefab) return '';
|
|
37737
|
+
return editor3DLevelJSON({...(prefab.attached ? {attached: true} : {}), objects: prefab.objects});
|
|
37738
|
+
}
|
|
37739
|
+
|
|
37740
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
37741
|
+
// Edit prefab: the editor on a prefab alone, the level it came from put away until Back
|
|
37742
|
+
|
|
37743
|
+
// the prefabs opened one inside the other, each with the level it was opened from, its name, the selection there
|
|
37744
|
+
// and the ids of that level's objects that had something made for them
|
|
37745
|
+
/** @type {Array<{level: Object, name: string, selection: Array<number>, made: Set<number>, entered?: string, saved?: string}>} */
|
|
37746
|
+
const editor3DPrefabStack = [];
|
|
37747
|
+
// the levels that are a prefab being edited, they have no autosave of their own
|
|
37748
|
+
const editor3DPrefabLevels = new WeakSet;
|
|
37749
|
+
// what was edited in prefabs of the level's own since the level was left, written into it on the way back
|
|
37750
|
+
/** @type {Object<string, Object>} */
|
|
37751
|
+
let editor3DPrefabEdits = {};
|
|
37752
|
+
// the prefabs of the game's own that were edited and are not in a file yet
|
|
37753
|
+
const editor3DPrefabDirty = new Set;
|
|
37754
|
+
|
|
37755
|
+
// destroy what the level being edited has in the world, its objects' and its maps'
|
|
37756
|
+
function editor3DPrefabClear()
|
|
37757
|
+
{
|
|
37758
|
+
for (const made of editor3DInstances.values())
|
|
37759
|
+
made?.destroy?.();
|
|
37760
|
+
editor3DInstances.clear();
|
|
37761
|
+
for (const map of [level3DVoxelMap, level3DTerrainMap])
|
|
37762
|
+
map && !map.destroyed && map.destroy();
|
|
37763
|
+
}
|
|
37764
|
+
|
|
37765
|
+
// open the prefab of an instance, the selected one when none is given, to edit alone: its objects about its own
|
|
37766
|
+
// origin, with an undo of its own, and the level it is in put away; false when it is not a prefab's instance
|
|
37767
|
+
function editor3DPrefabEnter(id)
|
|
37768
|
+
{
|
|
37769
|
+
const selected = editor3DSelected();
|
|
37770
|
+
id ??= selected.length === 1 ? selected[0].id : undefined;
|
|
37771
|
+
const made = editor3DInstances.get(id), from = editor3DLevel;
|
|
37772
|
+
const prefab = made instanceof Prefab3D && level3DPrefabs.get(made.prefabName);
|
|
37773
|
+
if (!prefab || editor3DRecords.get(from)?.pending) return false;
|
|
37774
|
+
editor3DStrokeEnd();
|
|
37775
|
+
editor3DDrag = editor3DHover = undefined;
|
|
37776
|
+
const frame = {level: from, name: made.prefabName, selection: [...editor3DSelection],
|
|
37777
|
+
made: new Set(editor3DInstances.keys()), entered: undefined};
|
|
37778
|
+
editor3DPrefabStack.push(frame);
|
|
37779
|
+
editor3DPrefabClear();
|
|
37780
|
+
const level = {littlejs3D: LEVEL3D_VERSION, ...(prefab.attached ? {attached: true} : {}),
|
|
37781
|
+
objects: editor3DCopy(prefab.objects)};
|
|
37782
|
+
editor3DPrefabLevels.add(level);
|
|
37783
|
+
level3DLoad(level); // the editor takes it as it does any level
|
|
37784
|
+
frame.entered = JSON.stringify(editor3DObjects()); // as the editor has it, an edit is what differs from this
|
|
37785
|
+
return true;
|
|
37786
|
+
}
|
|
37787
|
+
|
|
37788
|
+
// leave the prefab being edited for the level it was opened from: the prefab is what was edited, every instance
|
|
37789
|
+
// of it is made again, and back in the level itself what was edited in its own prefabs is one undo; false when
|
|
37790
|
+
// no prefab is open
|
|
37791
|
+
function editor3DPrefabBack()
|
|
37792
|
+
{
|
|
37793
|
+
const frame = editor3DPrefabStack.pop(), edited = editor3DLevel;
|
|
37794
|
+
if (!frame) return false;
|
|
37795
|
+
editor3DStrokeEnd();
|
|
37796
|
+
editor3DDrag = editor3DHover = undefined;
|
|
37797
|
+
const known = level3DPrefabs.get(frame.name);
|
|
37798
|
+
const objects = editor3DCopy(editor3DObjects()), changed = JSON.stringify(objects) !== frame.entered;
|
|
37799
|
+
editor3DPrefabClear();
|
|
37800
|
+
if (changed)
|
|
37801
|
+
{
|
|
37802
|
+
// the prefab as it is now, for whatever is made next; one of the game's own waits to be saved to its file
|
|
37803
|
+
const prefab = known?.attached ? {attached: true, objects} : {objects}, fromLevel = !!known?.fromLevel;
|
|
37804
|
+
level3DPrefabSet(frame.name, prefab, fromLevel);
|
|
37805
|
+
if (fromLevel)
|
|
37806
|
+
editor3DPrefabEdits[frame.name] = prefab;
|
|
37807
|
+
else if (JSON.stringify(objects) !== frame.saved)
|
|
37808
|
+
editor3DPrefabDirty.add(frame.name); // not what Save wrote while it was open
|
|
37809
|
+
}
|
|
37810
|
+
|
|
37811
|
+
// the level it was opened from is the editor's again, with its own undo
|
|
37812
|
+
const level = editor3DLevel = frame.level, record = editor3DRecords.get(level);
|
|
37813
|
+
editor3DUndoList = record.undo, editor3DRedoList = record.redo;
|
|
37814
|
+
editor3DStroke = undefined;
|
|
37815
|
+
if (!editor3DPrefabStack.length)
|
|
37816
|
+
{
|
|
37817
|
+
// back in the level: its own prefabs that were edited go into it, as one undo
|
|
37818
|
+
const edits = editor3DPrefabEdits;
|
|
37819
|
+
editor3DPrefabEdits = {};
|
|
37820
|
+
Object.keys(edits).length && editor3DChangePart('prefabs', (prefabs={})=> ({...prefabs, ...edits}));
|
|
37821
|
+
editor3DStrokeEnd();
|
|
37822
|
+
}
|
|
37823
|
+
// what the level had in the world is made again: what a class makes, and what else had something made
|
|
37824
|
+
for (const object of editor3DObjects())
|
|
37825
|
+
{
|
|
37826
|
+
const type = level3DTypes.get(object.type);
|
|
37827
|
+
type && (type.make.prototype || frame.made.has(object.id)) && editor3DMakeInstance(object);
|
|
37828
|
+
}
|
|
37829
|
+
editor3DSceneRestore(record.sceneBase);
|
|
37830
|
+
level3DSceneApply(level.scene);
|
|
37831
|
+
editor3DVoxelShow();
|
|
37832
|
+
editor3DTerrainShow();
|
|
37833
|
+
editor3DShadowLight();
|
|
37834
|
+
editor3DSelection.clear();
|
|
37835
|
+
for (const id of frame.selection)
|
|
37836
|
+
editor3DObject(id) && editor3DSelection.add(id);
|
|
37837
|
+
return true;
|
|
37838
|
+
}
|
|
37839
|
+
|
|
36239
37840
|
// remove the selected objects, as one undo
|
|
36240
37841
|
function editor3DDelete()
|
|
36241
37842
|
{
|
|
@@ -36255,6 +37856,9 @@ function editor3DCopySelection()
|
|
|
36255
37856
|
const selected = editor3DSelected();
|
|
36256
37857
|
if (!selected.length) return false;
|
|
36257
37858
|
editor3DClipboard = editor3DCopy(selected);
|
|
37859
|
+
// how big each was and where its box was about its place: what is cut has nothing left to measure
|
|
37860
|
+
editor3DClipboardBoxes = new Map(editor3DClipboard.map((copy, i)=>
|
|
37861
|
+
[copy, {size: editor3DSize(selected[i]), offset: editor3DBoxOffset(selected[i])}]));
|
|
36258
37862
|
}
|
|
36259
37863
|
|
|
36260
37864
|
// copy the selected objects and remove them
|
|
@@ -36306,11 +37910,17 @@ function editor3DRestore()
|
|
|
36306
37910
|
{
|
|
36307
37911
|
const type = level3DTypes.get(object.type), made = editor3DInstances.get(object.id);
|
|
36308
37912
|
if (!type) continue;
|
|
36309
|
-
|
|
37913
|
+
// a prefab's instance that play took a part of is made again, whole
|
|
37914
|
+
const broken = (p)=> p instanceof Prefab3D && p.parts.some((part)=> part?.destroyed || broken(part));
|
|
37915
|
+
if (made instanceof EngineObject3D && !made.destroyed && !broken(made))
|
|
36310
37916
|
editor3DPlaceInstance(made, object);
|
|
36311
|
-
else if (type.make.prototype && (!made || made.destroyed))
|
|
37917
|
+
else if (type.make.prototype && (!made || made.destroyed || broken(made)))
|
|
36312
37918
|
editor3DMakeInstance(object);
|
|
36313
37919
|
}
|
|
37920
|
+
// the blocks and the terrain as the level has them: what play dug, raised or destroyed is not the level's,
|
|
37921
|
+
// and must not be written into it by the next stroke
|
|
37922
|
+
editor3DVoxelShow();
|
|
37923
|
+
editor3DTerrainShow();
|
|
36314
37924
|
}
|
|
36315
37925
|
|
|
36316
37926
|
// open or close the editor, the session goes on: the game is paused under it and reads no input
|
|
@@ -36328,14 +37938,19 @@ function editor3DSetOpen(open)
|
|
|
36328
37938
|
setDebugOverlay(false); // out of the way of the level
|
|
36329
37939
|
editor3DIsOpen = true;
|
|
36330
37940
|
editor3DRestore();
|
|
37941
|
+
editorCall('onOpen');
|
|
37942
|
+
editorKeyClashes();
|
|
36331
37943
|
}
|
|
36332
37944
|
else
|
|
36333
37945
|
{
|
|
36334
37946
|
editor3DDrag = editor3DHover = undefined;
|
|
36335
37947
|
editor3DMouseOnPanel = false; // the panel hides, with no mouseleave
|
|
36336
37948
|
editor3DStrokeEnd();
|
|
37949
|
+
while (editor3DPrefabBack()); // the game plays the level, not a prefab that was open
|
|
37950
|
+
editor3DToolHeld = false;
|
|
36337
37951
|
editor3DIsOpen = false;
|
|
36338
37952
|
setPaused(editor3DGamePaused);
|
|
37953
|
+
editorCall('onClose');
|
|
36339
37954
|
}
|
|
36340
37955
|
inputCapture(editor3DViewOn());
|
|
36341
37956
|
}
|
|
@@ -36359,24 +37974,116 @@ function editor3DClose()
|
|
|
36359
37974
|
function editor3DPlay(pos)
|
|
36360
37975
|
{
|
|
36361
37976
|
editor3DSetOpen(false);
|
|
36362
|
-
editor3DPlayFromMouse && pos && levelEditor.onPlayFrom
|
|
37977
|
+
editor3DPlayFromMouse && pos && editorHas('onPlayFrom') && levelEditor.onPlayFrom(pos.copy());
|
|
36363
37978
|
}
|
|
36364
37979
|
|
|
36365
37980
|
// rebuild the level through the game's hook and play it
|
|
36366
37981
|
function editor3DRestart()
|
|
36367
37982
|
{
|
|
36368
|
-
if (!
|
|
37983
|
+
if (!editorHas('onRestart')) return;
|
|
36369
37984
|
editor3DSetOpen(false);
|
|
36370
37985
|
levelEditor.onRestart();
|
|
36371
37986
|
}
|
|
36372
37987
|
|
|
36373
|
-
// the
|
|
37988
|
+
// a key as addKey spells it, as the 3D editor's tables have it, by its position: a letter, a digit, a mark by the
|
|
37989
|
+
// key it is on, or its name
|
|
37990
|
+
const editor3DKeyMarks = {'?': 'Slash', '/': 'Slash', ',': 'Comma', '.': 'Period', ';': 'Semicolon', "'": 'Quote',
|
|
37991
|
+
'[': 'BracketLeft', ']': 'BracketRight', '-': 'Minus', '=': 'Equal', '\\': 'Backslash', '`': 'Backquote'};
|
|
37992
|
+
function editor3DKeyCode(name)
|
|
37993
|
+
{
|
|
37994
|
+
return name.length !== 1 ? name : editor3DKeyMarks[name] ??
|
|
37995
|
+
(name >= '0' && name <= '9' ? 'Digit' + name : 'Key' + name.toUpperCase());
|
|
37996
|
+
}
|
|
37997
|
+
|
|
37998
|
+
// the 3D editor's edit functions, what levelEditor.edit3D gives a game's own keys, buttons and tools
|
|
37999
|
+
const editor3DEdit =
|
|
38000
|
+
{
|
|
38001
|
+
get level() { return editor3DLevel; },
|
|
38002
|
+
get objects() { return editor3DObjects(); },
|
|
38003
|
+
get selection() { return editor3DSelection; },
|
|
38004
|
+
selected() { return editor3DSelected(); },
|
|
38005
|
+
made(id) { return editor3DInstances.get(id); },
|
|
38006
|
+
change(change) { return editor3DChange(change); },
|
|
38007
|
+
changePart(name, change) { return editor3DLevelPartNames.includes(name) && editor3DChangePart(name, change); },
|
|
38008
|
+
strokeEnd() { editor3DStrokeEnd(); },
|
|
38009
|
+
strokeCancel() { editor3DStrokeCancel(); },
|
|
38010
|
+
place(type, pos3D) { return level3DTypes.has(type) ? editor3DPlace(type, pos3D) : undefined; },
|
|
38011
|
+
setTransform(object, pos3D, rotation, scale3D) { editor3DSetTransform(object, pos3D, rotation, scale3D); },
|
|
38012
|
+
setProperty(object, name, value)
|
|
38013
|
+
{ editor3DSetProperty(object, name, value, level3DTypes.get(object.type)?.defaults[name]); },
|
|
38014
|
+
pos(object) { return editor3DPos(object); },
|
|
38015
|
+
rotation(object) { return editor3DRotation(object); },
|
|
38016
|
+
scale(object) { return editor3DScale(object); },
|
|
38017
|
+
mousePoint() { return editor3DIsOpen && !editor3DMouseOnPanel ? editor3DWithView(editor3DMousePoint) : undefined; },
|
|
38018
|
+
undo(redo=false) { return editor3DUndo(redo); },
|
|
38019
|
+
toJSON() { return editor3DLevel ? editor3DLevelJSON() : ''; },
|
|
38020
|
+
};
|
|
38021
|
+
|
|
38022
|
+
// the level editor's open, close and isOpen go to the 3D editor when the game has a 3D level, and it has the 3D
|
|
38023
|
+
// editor's keys and edit functions through this
|
|
36374
38024
|
if (debug)
|
|
38025
|
+
editorOther = {wanted: editor3DWanted, isOpen: ()=> editor3DIsOpen, active: ()=> editor3DIsOpen || editor3DSession,
|
|
38026
|
+
open: editor3DOpen, close: editor3DClose, edit: editor3DEdit,
|
|
38027
|
+
hasKey: (name, ctrl)=> !!(ctrl ? editor3DCtrlKeys : editor3DKeys)[editor3DKeyCode(name)]};
|
|
38028
|
+
|
|
38029
|
+
// pick a tool of the editor's own, which puts a tool of the game's down
|
|
38030
|
+
function editor3DPickTool(tool)
|
|
38031
|
+
{
|
|
38032
|
+
editor3DTool = tool;
|
|
38033
|
+
levelEditor.tool = undefined;
|
|
38034
|
+
}
|
|
38035
|
+
|
|
38036
|
+
// a tool of the game's own: if its press is held, and the editor's tool and type to place when it was turned on,
|
|
38037
|
+
// since picking one of those puts the game's tool down
|
|
38038
|
+
let editor3DToolHeld = false, editor3DToolName, editor3DToolPicked = [];
|
|
38039
|
+
|
|
38040
|
+
// what a tool's callbacks are given: where the mouse is on the level or the ground, its ray, and the keys held
|
|
38041
|
+
function editor3DToolAt(ray=render3D.screenToRay(mousePosScreen), shift=false, ctrl=false)
|
|
38042
|
+
{ return {pos: editor3DMouseOnPanel ? undefined : editor3DSurface(ray), cell: undefined, ray, shift, ctrl}; }
|
|
38043
|
+
|
|
38044
|
+
// the game's tool, when one is on: the left button is its, a press, a drag and a release one undo, the right
|
|
38045
|
+
// button takes a held press back; true when it has the mouse
|
|
38046
|
+
function editor3DToolUpdate(ray, idle, shift, ctrl)
|
|
38047
|
+
{
|
|
38048
|
+
const picked = [editor3DTool, editor3DBrush];
|
|
38049
|
+
if (levelEditor.tool !== editor3DToolName)
|
|
38050
|
+
editor3DToolName = levelEditor.tool, editor3DToolPicked = picked; // turned on, or off, by its key or button
|
|
38051
|
+
else if (picked.some((value, i)=> value !== editor3DToolPicked[i]))
|
|
38052
|
+
levelEditor.tool = editor3DToolName = undefined; // a tool or a type of the editor's was picked
|
|
38053
|
+
const tool = editorGameTool();
|
|
38054
|
+
if (!tool)
|
|
38055
|
+
{
|
|
38056
|
+
editor3DToolHeld = false;
|
|
38057
|
+
return false;
|
|
38058
|
+
}
|
|
38059
|
+
const at = editor3DToolAt(ray, shift, ctrl);
|
|
38060
|
+
editor3DHover = editor3DBlockHover = editor3DTerrainHover = undefined; // the editor's own cursors are put away
|
|
38061
|
+
if (editor3DToolHeld && mouseWasPressed(2))
|
|
38062
|
+
{
|
|
38063
|
+
editor3DStrokeCancel();
|
|
38064
|
+
editor3DToolHeld = false;
|
|
38065
|
+
}
|
|
38066
|
+
else if (idle && mouseWasPressed(0))
|
|
38067
|
+
editor3DToolHeld = tool.onPress?.(at) !== false;
|
|
38068
|
+
else if (editor3DToolHeld && mouseIsDown(0))
|
|
38069
|
+
tool.onDrag?.(at);
|
|
38070
|
+
if (editor3DToolHeld && !mouseIsDown(0))
|
|
38071
|
+
{
|
|
38072
|
+
tool.onRelease?.(at);
|
|
38073
|
+
editor3DStrokeEnd();
|
|
38074
|
+
editor3DToolHeld = false;
|
|
38075
|
+
}
|
|
38076
|
+
return true;
|
|
38077
|
+
}
|
|
38078
|
+
|
|
38079
|
+
// the game's own drawing in the level, in the editor's 3D pass: the editor's onDraw, and its tool's while one is on
|
|
38080
|
+
function editor3DDrawGame()
|
|
36375
38081
|
{
|
|
36376
|
-
|
|
36377
|
-
|
|
36378
|
-
|
|
36379
|
-
|
|
38082
|
+
if (!editor3DIsOpen) return;
|
|
38083
|
+
editorCall('onDraw');
|
|
38084
|
+
const held = (...codes)=> inputCaptureRead(()=> codes.some((code)=> keyIsDown(code)));
|
|
38085
|
+
editorGameTool()?.onDraw?.(editor3DToolAt(undefined, held('ShiftLeft', 'ShiftRight'),
|
|
38086
|
+
held('ControlLeft', 'ControlRight', 'MetaLeft', 'MetaRight')));
|
|
36380
38087
|
}
|
|
36381
38088
|
|
|
36382
38089
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -36413,9 +38120,25 @@ const editor3DSaves = ()=> readSaveData(editor3DSaveName(), {});
|
|
|
36413
38120
|
let editor3DSaveFailed = false;
|
|
36414
38121
|
|
|
36415
38122
|
// remember the level's objects and parts, or forget them when they are back to the file
|
|
36416
|
-
function editor3DAutosave(level=editor3DLevel)
|
|
38123
|
+
function editor3DAutosave(level=editor3DLevel, known)
|
|
36417
38124
|
{
|
|
36418
|
-
|
|
38125
|
+
// a prefab being edited is kept with the level it is in, as that level would be on going back: its own
|
|
38126
|
+
// prefabs with what is open, and what was edited on the way, in them
|
|
38127
|
+
if (editor3DPrefabLevels.has(level))
|
|
38128
|
+
{
|
|
38129
|
+
const root = editor3DPrefabStack[0]?.level, edits = {...editor3DPrefabEdits};
|
|
38130
|
+
editor3DPrefabStack.forEach((frame, i)=>
|
|
38131
|
+
{
|
|
38132
|
+
const open = editor3DPrefabStack[i + 1]?.level ?? editor3DLevel, known = level3DPrefabs.get(frame.name);
|
|
38133
|
+
const objects = isArray(open.objects) ? editor3DCopy(open.objects) : [];
|
|
38134
|
+
if (known?.fromLevel && JSON.stringify(objects) !== frame.entered)
|
|
38135
|
+
edits[frame.name] = known.attached ? {attached: true, objects} : {objects};
|
|
38136
|
+
});
|
|
38137
|
+
if (root && Object.keys(edits).length)
|
|
38138
|
+
editor3DAutosave({...root, prefabs: {...editor3DLevelPart('prefabs', root), ...edits}}, editor3DRecords.get(root));
|
|
38139
|
+
return;
|
|
38140
|
+
}
|
|
38141
|
+
const record = known ?? editor3DRecords.get(level);
|
|
36419
38142
|
if (!record || record.pending) return; // edits waiting to be applied keep their autosave
|
|
36420
38143
|
const saves = editor3DSaves(), objects = isArray(level.objects) ? level.objects : [];
|
|
36421
38144
|
const parts = editor3DLevelParts(level);
|
|
@@ -36482,11 +38205,36 @@ async function editor3DSave(pickAgain=false)
|
|
|
36482
38205
|
const level = editor3DLevel, record = editor3DRecords.get(level);
|
|
36483
38206
|
if (!record) return;
|
|
36484
38207
|
editor3DStrokeEnd();
|
|
38208
|
+
const open = editor3DPrefabStack[editor3DPrefabStack.length - 1];
|
|
38209
|
+
if (open)
|
|
38210
|
+
{
|
|
38211
|
+
// inside a prefab, Save writes the prefab as a file of its own, or the game keeps it itself
|
|
38212
|
+
const text = editor3DLevelJSON(level), name = open.name + '.json';
|
|
38213
|
+
const written = JSON.stringify(editor3DObjects()); // as the file has it
|
|
38214
|
+
const saved = (async ()=>
|
|
38215
|
+
{
|
|
38216
|
+
await record.saving; // in the order asked for, as a level's saves are
|
|
38217
|
+
const kept = await editorCall('onSave', text, name) === true;
|
|
38218
|
+
kept || saveText(text, name, 'application/json');
|
|
38219
|
+
editor3DPrefabDirty.delete(open.name);
|
|
38220
|
+
open.saved = written;
|
|
38221
|
+
return kept ? 'kept' : 'downloaded';
|
|
38222
|
+
})();
|
|
38223
|
+
record.saving = saved.catch(()=> {});
|
|
38224
|
+
return saved;
|
|
38225
|
+
}
|
|
36485
38226
|
// what is written, kept as it is now: the level can change while the file is picked and written, and those
|
|
36486
38227
|
// edits are not in the file, so they stay in the autosave; the level and record are this one's, whichever is
|
|
36487
38228
|
// open by then
|
|
36488
38229
|
const text = editor3DLevelJSON(level);
|
|
36489
|
-
|
|
38230
|
+
// its place in line is taken now, and the game's hook is asked when its turn comes, so saves are written in
|
|
38231
|
+
// the order they were asked for however long a hook takes
|
|
38232
|
+
const saved = (async ()=>
|
|
38233
|
+
{
|
|
38234
|
+
await record.saving;
|
|
38235
|
+
if (await editorCall('onSave', text, record.fileName) === true) return 'kept'; // the game kept it itself
|
|
38236
|
+
return editor3DSaveText(level, record, text, pickAgain);
|
|
38237
|
+
})();
|
|
36490
38238
|
record.saving = saved.catch(()=> {});
|
|
36491
38239
|
return saved;
|
|
36492
38240
|
}
|
|
@@ -36546,6 +38294,8 @@ const EDITOR3D_AXES = {x: vec3(1, 0, 0), y: vec3(0, 1, 0), z: vec3(0, 0, 1)};
|
|
|
36546
38294
|
|
|
36547
38295
|
let editor3DTool = 'move'; // 'select', 'move', 'rotate', 'scale', 'blocks' or 'terrain'
|
|
36548
38296
|
let editor3DBlockType = 1; // the block type the Blocks tool places
|
|
38297
|
+
let editor3DBlockBox = false; // the Blocks tool fills a box: the rectangle dragged, editor3DBlockBoxHeight tall
|
|
38298
|
+
let editor3DBlockBoxHeight = 1;
|
|
36549
38299
|
let editor3DSeconds = 0; // how long the last step was, for a brush held down
|
|
36550
38300
|
// where the mouse is on the terrain, for the Terrain tool's brush
|
|
36551
38301
|
/** @type {Vector3|undefined} */
|
|
@@ -36555,6 +38305,7 @@ let editor3DTerrainHover;
|
|
|
36555
38305
|
let editor3DBlockHover;
|
|
36556
38306
|
let editor3DGrid = true; // moves, turns and sizes go in steps, Ctrl flips it for a drag
|
|
36557
38307
|
let editor3DGroundSnap = true; // a body drag slides along what is under the mouse
|
|
38308
|
+
let editor3DLocalAxes = false; // the Move handles follow the object's own axes, not the world's
|
|
36558
38309
|
let editor3DMoveStep = 1, editor3DRotateStep = 15, editor3DScaleStep = .25; // the rotate step in degrees
|
|
36559
38310
|
let editor3DHelp = false; // the keys are shown
|
|
36560
38311
|
let editor3DMouseOnPanel = false;
|
|
@@ -36582,7 +38333,7 @@ function editor3DSelectionCenter()
|
|
|
36582
38333
|
function editor3DHandles()
|
|
36583
38334
|
{
|
|
36584
38335
|
const center = editor3DSelectionCenter(), tool = editor3DTool, handles = [];
|
|
36585
|
-
if (!center || tool === 'select' || tool === 'blocks' || tool === 'terrain') return handles;
|
|
38336
|
+
if (!center || tool === 'select' || tool === 'blocks' || tool === 'terrain' || editorGameTool()) return handles;
|
|
36586
38337
|
const length = editor3DScreenScale(center) * EDITOR3D_HANDLE_SIZE;
|
|
36587
38338
|
const rotation = editor3DRotation(editor3DSelected()[0]).scale(PI / 180), frame = buildMatrix(vec3(), rotation);
|
|
36588
38339
|
for (const axis of 'xyz')
|
|
@@ -36599,16 +38350,19 @@ function editor3DHandles()
|
|
|
36599
38350
|
continue;
|
|
36600
38351
|
}
|
|
36601
38352
|
|
|
36602
|
-
// an arrow or scale handle starts a little out from the middle, which is left for the object's body
|
|
36603
|
-
|
|
36604
|
-
|
|
38353
|
+
// an arrow or scale handle starts a little out from the middle, which is left for the object's body; Move
|
|
38354
|
+
// goes along the world's axes, or with own axes on along the object's, as Scale always does
|
|
38355
|
+
const local = tool !== 'move' || editor3DLocalAxes;
|
|
38356
|
+
const way = (k)=> local ? frame.transformDirection(EDITOR3D_AXES[k]) : EDITOR3D_AXES[k];
|
|
38357
|
+
const direction = way(axis), turn = tool === 'move' && local ? rotation : undefined;
|
|
38358
|
+
handles.push({kind: tool === 'move' ? 'arrow' : 'scale', axis, center, length, direction, turn,
|
|
36605
38359
|
points: [center.add(direction.scale(length * .2)), center.add(direction.scale(length))]});
|
|
36606
38360
|
if (tool === 'move')
|
|
36607
38361
|
{
|
|
36608
|
-
// the square between the other two axes moves along both
|
|
38362
|
+
// the square between the other two axes moves along both; across is the axis it does not move along
|
|
36609
38363
|
const [a, b] = 'xyz'.replace(axis, '');
|
|
36610
|
-
handles.push({kind: 'plane', axis: a + b, center, length, direction,
|
|
36611
|
-
points: [center.add(
|
|
38364
|
+
handles.push({kind: 'plane', axis: a + b, center, length, direction, turn, across: axis,
|
|
38365
|
+
ways: [way(a), way(b)], points: [center.add(way(a).add(way(b)).scale(length * .35))]});
|
|
36612
38366
|
}
|
|
36613
38367
|
}
|
|
36614
38368
|
tool === 'scale' && handles.push({kind: 'scaleAll', axis: 'xyz', center, length, points: [center]});
|
|
@@ -36675,6 +38429,10 @@ function editor3DParts(made, parts=[])
|
|
|
36675
38429
|
parts.push(made);
|
|
36676
38430
|
for (const child of made.children)
|
|
36677
38431
|
editor3DParts(child, parts);
|
|
38432
|
+
// a prefab's instance and its parts are one thing, the ones that are not its children too
|
|
38433
|
+
if (made instanceof Prefab3D)
|
|
38434
|
+
for (const part of made.parts)
|
|
38435
|
+
part?.parent === made || editor3DParts(part, parts);
|
|
36678
38436
|
}
|
|
36679
38437
|
return parts;
|
|
36680
38438
|
}
|
|
@@ -36748,10 +38506,13 @@ function editor3DPlaceAt(type, ray)
|
|
|
36748
38506
|
editor3DStrokeEnd();
|
|
36749
38507
|
const id = editor3DPlace(type, point);
|
|
36750
38508
|
if (id === undefined) return;
|
|
36751
|
-
const
|
|
38509
|
+
const object = editor3DObject(id), size = editor3DSize(object), offset = editor3DBoxOffset(object);
|
|
38510
|
+
const ignore = new Set(editor3DParts(editor3DInstances.get(id)));
|
|
36752
38511
|
const snapped = editor3DSnapPos(editor3DRest(point, size), size, editor3DGrid ? editor3DMoveStep : 0);
|
|
36753
38512
|
const y = editor3DLand(snapped.x, snapped.z, point.y, size, ignore);
|
|
36754
|
-
|
|
38513
|
+
// the box stands there, and the object's place is where that puts it
|
|
38514
|
+
const to = vec3(snapped.x, y, snapped.z).subtract(offset);
|
|
38515
|
+
editor3DChange((list)=> editor3DSetTransform(list.find((o)=> o.id === id), to));
|
|
36755
38516
|
editor3DStrokeEnd();
|
|
36756
38517
|
}
|
|
36757
38518
|
|
|
@@ -36761,7 +38522,7 @@ function editor3DDragTo(drag, mouse, ray, snap)
|
|
|
36761
38522
|
if (drag.kind === 'blocks')
|
|
36762
38523
|
return editor3DVoxelDragTo(drag, ray);
|
|
36763
38524
|
if (drag.kind === 'terrain')
|
|
36764
|
-
return editor3DTerrainHover &&
|
|
38525
|
+
return editor3DTerrainHover && editor3DTerrainDragTo(drag, editor3DTerrainHover);
|
|
36765
38526
|
if (drag.kind === 'box')
|
|
36766
38527
|
{
|
|
36767
38528
|
drag.to = mouse.copy();
|
|
@@ -36772,18 +38533,23 @@ function editor3DDragTo(drag, mouse, ray, snap)
|
|
|
36772
38533
|
const was = first && start.get(first.id);
|
|
36773
38534
|
if (!was) return;
|
|
36774
38535
|
const size = editor3DSize(first), turned = editor3DTurned(first), step = snap ? editor3DMoveStep : 0;
|
|
36775
|
-
const
|
|
38536
|
+
const offset = editor3DBoxOffset(first); // snapping and landing go by its box
|
|
38537
|
+
const snapTo = (to)=> editor3DSnapPos(to.add(offset), size, step, turned).subtract(offset).subtract(was.pos);
|
|
36776
38538
|
let move, turn, factor;
|
|
36777
38539
|
if (kind === 'arrow')
|
|
36778
38540
|
{
|
|
36779
38541
|
const d = editor3DAxisDistance(ray, handle.center, handle.direction) - grab;
|
|
36780
|
-
move
|
|
38542
|
+
// an axis of its own does not follow the grid, so the move goes in steps from where it started
|
|
38543
|
+
move = handle.turn ? handle.direction.scale(step ? round(d / step) * step : d) :
|
|
38544
|
+
handle.direction.multiply(snapTo(was.pos.add(handle.direction.scale(d)))); // along its axis alone
|
|
36781
38545
|
}
|
|
36782
38546
|
else if (kind === 'plane')
|
|
36783
38547
|
{
|
|
36784
38548
|
const point = editor3DPlanePoint(ray, handle.center, handle.direction);
|
|
36785
38549
|
if (!point || !grab) return;
|
|
36786
|
-
|
|
38550
|
+
const delta = point.subtract(grab), along = (w)=> step ? round(delta.dot(w) / step) * step : delta.dot(w);
|
|
38551
|
+
move = handle.turn ? handle.ways[0].scale(along(handle.ways[0])).add(handle.ways[1].scale(along(handle.ways[1]))) :
|
|
38552
|
+
vec3(1).subtract(handle.direction).multiply(snapTo(was.pos.add(delta)));
|
|
36787
38553
|
}
|
|
36788
38554
|
else if (kind === 'body')
|
|
36789
38555
|
{
|
|
@@ -36791,8 +38557,8 @@ function editor3DDragTo(drag, mouse, ray, snap)
|
|
|
36791
38557
|
const point = editor3DPlanePoint(ray, drag.level, drag.normal);
|
|
36792
38558
|
if (!point) return;
|
|
36793
38559
|
const across = snapTo(was.pos.add(point.subtract(drag.level)));
|
|
36794
|
-
const y = editor3DGroundSnap ? editor3DLand(was.pos.x + across.x, was.pos.z + across.z,
|
|
36795
|
-
was.pos.y - size.y / 2, size, drag.ignore) : was.pos.y;
|
|
38560
|
+
const y = editor3DGroundSnap ? editor3DLand(was.pos.x + offset.x + across.x, was.pos.z + offset.z + across.z,
|
|
38561
|
+
was.pos.y + offset.y - size.y / 2, size, drag.ignore) - offset.y : was.pos.y;
|
|
36796
38562
|
move = vec3(across.x, y - was.pos.y, across.z);
|
|
36797
38563
|
}
|
|
36798
38564
|
else if (kind === 'ring')
|
|
@@ -36893,9 +38659,9 @@ function editor3DDropSelection()
|
|
|
36893
38659
|
{
|
|
36894
38660
|
for (const object of list)
|
|
36895
38661
|
{
|
|
36896
|
-
const to = editor3DSelection.has(object.id) &&
|
|
36897
|
-
editor3DDrop(editor3DPos(object), editor3DSize(object), ignore);
|
|
36898
|
-
to && editor3DSetTransform(object, to);
|
|
38662
|
+
const offset = editor3DBoxOffset(object), to = editor3DSelection.has(object.id) &&
|
|
38663
|
+
editor3DDrop(editor3DPos(object).add(offset), editor3DSize(object), ignore);
|
|
38664
|
+
to && editor3DSetTransform(object, to.subtract(offset));
|
|
36899
38665
|
}
|
|
36900
38666
|
});
|
|
36901
38667
|
editor3DStrokeEnd();
|
|
@@ -36909,11 +38675,12 @@ function editor3DPasteAtMouse()
|
|
|
36909
38675
|
if (!copied?.length) return false;
|
|
36910
38676
|
if (!point) return editor3DPaste();
|
|
36911
38677
|
const center = copied.reduce((sum, o)=> sum.add(editor3DPos(o)), vec3()).scale(1 / copied.length);
|
|
36912
|
-
const
|
|
36913
|
-
const
|
|
36914
|
-
const
|
|
38678
|
+
const box = (o)=> editor3DClipboardBoxes.get(o) ?? {size: editor3DSize(o), offset: editor3DBoxOffset(o)};
|
|
38679
|
+
const bottom = copied.reduce((low, o)=> min(low, editor3DPos(o).y + box(o).offset.y - box(o).size.y / 2), Infinity);
|
|
38680
|
+
const first = copied[0], size = box(first).size, was = editor3DPos(first).add(box(first).offset); // its box's middle
|
|
38681
|
+
const snapped = editor3DSnapPos(was.add(vec3(point.x - center.x, 0, point.z - center.z)), size,
|
|
36915
38682
|
editor3DGrid ? editor3DMoveStep : 0, editor3DTurned(first));
|
|
36916
|
-
const land = editor3DLand(snapped.x, snapped.z, point.y,
|
|
38683
|
+
const land = editor3DLand(snapped.x, snapped.z, point.y, size) - size.y / 2;
|
|
36917
38684
|
return editor3DPaste(vec3(snapped.x - was.x, land - bottom, snapped.z - was.z));
|
|
36918
38685
|
}
|
|
36919
38686
|
|
|
@@ -36922,18 +38689,22 @@ function editor3DPasteAtMouse()
|
|
|
36922
38689
|
/** @type {Object<string, function(boolean=): any>} */
|
|
36923
38690
|
const editor3DKeys =
|
|
36924
38691
|
{
|
|
36925
|
-
KeyQ: ()=> {
|
|
36926
|
-
KeyW: ()=> {
|
|
36927
|
-
KeyE: ()=> {
|
|
36928
|
-
KeyR: ()=> {
|
|
36929
|
-
KeyB: ()=> {
|
|
36930
|
-
KeyT: ()=> {
|
|
38692
|
+
KeyQ: ()=> { editor3DPickTool('select'); editor3DBrush = undefined; },
|
|
38693
|
+
KeyW: ()=> { editor3DPickTool('move'); },
|
|
38694
|
+
KeyE: ()=> { editor3DPickTool('rotate'); },
|
|
38695
|
+
KeyR: ()=> { editor3DPickTool('scale'); },
|
|
38696
|
+
KeyB: ()=> { editor3DPickTool('blocks'); editor3DBrush = undefined; },
|
|
38697
|
+
KeyT: ()=> { editor3DPickTool('terrain'); editor3DBrush = undefined; },
|
|
36931
38698
|
KeyP: ()=> editor3DTool === 'blocks' && editor3DVoxelPick(render3D.screenToRay(mousePosScreen)),
|
|
38699
|
+
KeyX: ()=> editor3DTool === 'terrain' ? editor3DTerrainNextBrush() :
|
|
38700
|
+
editor3DTool === 'blocks' && (editor3DBlockBox = !editor3DBlockBox),
|
|
36932
38701
|
KeyG: ()=> { editor3DGrid = !editor3DGrid; },
|
|
38702
|
+
KeyL: ()=> { editor3DLocalAxes = !editor3DLocalAxes; },
|
|
36933
38703
|
KeyF: ()=> editor3DFrame(),
|
|
36934
38704
|
End: ()=> editor3DDropSelection(),
|
|
36935
38705
|
Delete: ()=> editor3DDelete(),
|
|
36936
|
-
Backspace: ()=> editor3DDelete(),
|
|
38706
|
+
Backspace: ()=> editor3DSelection.size ? editor3DDelete() : editor3DPrefabBack(),
|
|
38707
|
+
Enter: ()=> editor3DPrefabEnter(),
|
|
36937
38708
|
Slash: ()=> { editor3DHelp = !editor3DHelp; },
|
|
36938
38709
|
};
|
|
36939
38710
|
/** @type {Object<string, function(boolean=): any>} */
|
|
@@ -36945,6 +38716,7 @@ const editor3DCtrlKeys =
|
|
|
36945
38716
|
KeyX: ()=> editor3DCut(),
|
|
36946
38717
|
KeyV: ()=> editor3DPasteAtMouse(),
|
|
36947
38718
|
KeyD: ()=> editor3DDuplicate(),
|
|
38719
|
+
KeyG: (shift)=> shift ? editor3DUnpack() : editor3DMakePrefab() === true,
|
|
36948
38720
|
};
|
|
36949
38721
|
|
|
36950
38722
|
// the camera: the right button looks and the keys fly while it is held, the middle button or Space and the left
|
|
@@ -36997,7 +38769,13 @@ function editor3DEditorUpdate(seconds)
|
|
|
36997
38769
|
if (debugKey && keyWasPressed(debugKey))
|
|
36998
38770
|
{
|
|
36999
38771
|
inputClearKey(debugKey);
|
|
37000
|
-
|
|
38772
|
+
if (editor3DToolHeld)
|
|
38773
|
+
{
|
|
38774
|
+
editor3DStrokeCancel(); // a held press of the game's tool is taken back
|
|
38775
|
+
editor3DToolHeld = false;
|
|
38776
|
+
}
|
|
38777
|
+
else
|
|
38778
|
+
editor3DDrag ? editor3DDragCancel() : editor3DPlay(editor3DMousePoint());
|
|
37001
38779
|
return;
|
|
37002
38780
|
}
|
|
37003
38781
|
|
|
@@ -37013,9 +38791,20 @@ function editor3DEditorUpdate(seconds)
|
|
|
37013
38791
|
const looking = mouseIsDown(2), cameraDrag = looking || mouseIsDown(1) || keyIsDown('Space') || alt;
|
|
37014
38792
|
|
|
37015
38793
|
// the keys, which wait for a drag to end, and fly while the right button is held
|
|
37016
|
-
if (!editor3DDrag && !looking)
|
|
38794
|
+
if (!editor3DDrag && !looking && !editor3DToolHeld)
|
|
38795
|
+
{
|
|
38796
|
+
// the game's own keys first, each takes the place of the editor's key of the same spelling
|
|
38797
|
+
const taken = new Set;
|
|
38798
|
+
for (const [spelled, key] of Object.entries(levelEditor.keys))
|
|
38799
|
+
{
|
|
38800
|
+
const withCtrl = spelled.startsWith('ctrl+'), code = editor3DKeyCode(spelled.slice(withCtrl ? 5 : 0));
|
|
38801
|
+
if (withCtrl !== ctrl) continue;
|
|
38802
|
+
taken.add(code);
|
|
38803
|
+
keyWasPressed(code) && key.action.call(levelEditor, shift);
|
|
38804
|
+
}
|
|
37017
38805
|
for (const [code, action] of Object.entries(ctrl ? editor3DCtrlKeys : editor3DKeys))
|
|
37018
|
-
keyWasPressed(code) && action(shift);
|
|
38806
|
+
taken.has(code) || keyWasPressed(code) && action(shift);
|
|
38807
|
+
}
|
|
37019
38808
|
|
|
37020
38809
|
// a right press during a drag puts it back
|
|
37021
38810
|
if (editor3DDrag && mouseWasPressed(2))
|
|
@@ -37023,22 +38812,24 @@ function editor3DEditorUpdate(seconds)
|
|
|
37023
38812
|
|
|
37024
38813
|
const ray = render3D.screenToRay(mouse), idle = !cameraDrag && !editor3DMouseOnPanel && !editor3DDrag;
|
|
37025
38814
|
editor3DHover = idle ? editor3DHandleAt(mouse) : undefined;
|
|
38815
|
+
editorCall('onUpdate');
|
|
38816
|
+
if (editor3DToolUpdate(ray, idle, shift, ctrl)) return; // a tool of the game's own has the mouse
|
|
37026
38817
|
|
|
37027
38818
|
// the Blocks tool takes the mouse while the level has a map: Shift removes, Ctrl repaints
|
|
37028
|
-
|
|
38819
|
+
// a type picked to place is placed, whichever tool is on
|
|
38820
|
+
const painting = editor3DTool === 'blocks' && !editor3DBrush && !!editor3DVoxelMap();
|
|
37029
38821
|
const mode = shift ? 'remove' : ctrl ? 'paint' : 'place';
|
|
37030
38822
|
const target = painting && idle ? editor3DVoxelTarget(ray, mode) : undefined;
|
|
37031
38823
|
editor3DBlockHover = target && {cell: target.cell, mode};
|
|
37032
|
-
// the Terrain tool takes it while the level has a terrain: held down
|
|
37033
|
-
|
|
37034
|
-
const terrain = editor3DTool === 'terrain' ? editor3DTerrainMap() : undefined;
|
|
38824
|
+
// the Terrain tool takes it while the level has a terrain: held down its brush works on the ground
|
|
38825
|
+
const terrain = editor3DTool === 'terrain' && !editor3DBrush ? editor3DTerrainMap() : undefined;
|
|
37035
38826
|
const reach = terrain && !cameraDrag && !editor3DMouseOnPanel ? terrain.raycast(ray) : undefined;
|
|
37036
38827
|
editor3DTerrainHover = reach === undefined ? undefined : ray.getPosition(reach);
|
|
37037
38828
|
if (painting)
|
|
37038
38829
|
idle && mouseWasPressed(0) && (editor3DDrag = editor3DVoxelPress(ray, mode));
|
|
37039
38830
|
else if (terrain)
|
|
37040
38831
|
idle && mouseWasPressed(0) && editor3DTerrainHover &&
|
|
37041
|
-
(editor3DDrag =
|
|
38832
|
+
(editor3DDrag = editor3DTerrainPress(editor3DTerrainHover, shift, ctrl));
|
|
37042
38833
|
else if (idle && mouseWasPressed(0))
|
|
37043
38834
|
editor3DPress(mouse, ray, shift);
|
|
37044
38835
|
|
|
@@ -37052,7 +38843,7 @@ function editor3DEditorUpdate(seconds)
|
|
|
37052
38843
|
if (debug && globalThis.document?.addEventListener)
|
|
37053
38844
|
document.addEventListener('keydown', (e)=>
|
|
37054
38845
|
{
|
|
37055
|
-
if (editor3DIsOpen && (e.ctrlKey || e.metaKey) && /^Key[
|
|
38846
|
+
if (editor3DIsOpen && (e.ctrlKey || e.metaKey) && /^Key[ZYCXVDG]$/.test(e.code) && !editorIsTextField(e.target))
|
|
37056
38847
|
e.preventDefault();
|
|
37057
38848
|
});
|
|
37058
38849
|
|
|
@@ -37064,13 +38855,16 @@ const editor3DHelpLines =
|
|
|
37064
38855
|
[
|
|
37065
38856
|
'Left: select · Shift+Left: add or take away · Left drag from empty space: box select',
|
|
37066
38857
|
'Q select · W move · E rotate · R scale: drag a handle, or the object itself',
|
|
37067
|
-
'G: grid snap, Ctrl flips it for a drag · End: drop to the ground',
|
|
38858
|
+
'G: grid snap, Ctrl flips it for a drag · L: move along its own axes · End: drop to the ground',
|
|
37068
38859
|
'Right button: look, and WASD and QE fly while it is held, Shift faster',
|
|
37069
38860
|
'Wheel: zoom · Middle drag or Space+drag: pan · Alt+drag: orbit · F: frame the selection',
|
|
37070
38861
|
'Pick a type, then click to place it, Shift+click keeps placing',
|
|
37071
38862
|
'B blocks: click or drag places · Shift removes · Ctrl repaints · P picks the type under the mouse',
|
|
37072
|
-
'
|
|
38863
|
+
'X box fill: a drag fills the rectangle dragged, as tall as the height in the panel',
|
|
38864
|
+
'T terrain: hold to raise the ground · Shift lowers · Ctrl smooths · X the next brush, flatten and paint',
|
|
37073
38865
|
'Delete · Ctrl+C / X / V: copy, cut, paste · Ctrl+D: duplicate · Ctrl+Z / Y: undo, redo',
|
|
38866
|
+
'Ctrl+G: make the selection a prefab, one thing to place many times · Ctrl+Shift+G: unpack it into its objects',
|
|
38867
|
+
'Enter: open the selected prefab to edit it alone, every instance follows · Backspace with nothing selected: back',
|
|
37074
38868
|
'Reset to file: the level as its file has it, Restart keeps your edits, Undo brings them back',
|
|
37075
38869
|
'Esc: play and edit · 0: exit the editor · ?: keys',
|
|
37076
38870
|
];
|
|
@@ -37080,6 +38874,7 @@ function editor3DHint()
|
|
|
37080
38874
|
{
|
|
37081
38875
|
if (editor3DRecords.get(editor3DLevel)?.pending)
|
|
37082
38876
|
return 'The level file changed: apply your edits or drop them';
|
|
38877
|
+
if (editorGameTool()) return editorGameTool().hint || levelEditor.tool;
|
|
37083
38878
|
if (editor3DDrag)
|
|
37084
38879
|
return editor3DDrag.kind === 'box' ? 'Let go to select what is inside' :
|
|
37085
38880
|
'Esc or right click puts it back · Ctrl flips the snap';
|
|
@@ -37088,10 +38883,14 @@ function editor3DHint()
|
|
|
37088
38883
|
if (editor3DBrush)
|
|
37089
38884
|
return `Click to place a ${editor3DBrush} · Shift keeps placing · Q puts it down`;
|
|
37090
38885
|
if (editor3DTool === 'terrain')
|
|
37091
|
-
return editor3DTerrainMap() ? '
|
|
37092
|
-
'
|
|
38886
|
+
return !editor3DTerrainMap() ? 'Add a terrain in the panel to sculpt it' :
|
|
38887
|
+
{sculpt: 'Hold to raise the ground · Shift lowers · Ctrl smooths',
|
|
38888
|
+
flatten: 'Hold to flatten the ground to the height pressed · Ctrl smooths',
|
|
38889
|
+
paint: 'Hold to paint the ground · Shift takes the paint off'}[editor3DTerrainBrush.mode] +
|
|
38890
|
+
' · X the next brush';
|
|
37093
38891
|
if (editor3DTool === 'blocks')
|
|
37094
|
-
return editor3DVoxelMap() ?
|
|
38892
|
+
return editor3DVoxelMap() ? (editor3DBlockBox ? 'Drag a box to fill it' : 'Click or drag places') +
|
|
38893
|
+
' · Shift removes · Ctrl repaints · P picks a type · X box' :
|
|
37095
38894
|
'Add a block map in the panel to paint blocks';
|
|
37096
38895
|
if (editor3DSelection.size)
|
|
37097
38896
|
return 'Drag a handle or the object · W move · E rotate · R scale · End drops it';
|
|
@@ -37165,7 +38964,8 @@ function editor3DDrawRing(center, across, along, radius, color, pixels=2)
|
|
|
37165
38964
|
function editor3DVisible(object)
|
|
37166
38965
|
{
|
|
37167
38966
|
const made = editor3DInstances.get(object.id);
|
|
37168
|
-
return made instanceof EngineObject3D && !made.destroyed && (made.mesh || made.tileInfo
|
|
38967
|
+
return made instanceof EngineObject3D && !made.destroyed && (made.mesh || made.tileInfo || editor3DPrefabBox(made)) ?
|
|
38968
|
+
made : undefined;
|
|
37169
38969
|
}
|
|
37170
38970
|
|
|
37171
38971
|
// the box around an object as a matrix: around its mesh where it is, turned and sized as it is, a unit box for a
|
|
@@ -37175,6 +38975,9 @@ function editor3DBoxMatrix(object)
|
|
|
37175
38975
|
const made = editor3DVisible(object), bounds = made?.mesh && (made.mesh.bounds || made.mesh.getBounds());
|
|
37176
38976
|
if (!made)
|
|
37177
38977
|
return buildMatrix(editor3DPos(object));
|
|
38978
|
+
const box = editor3DPrefabBox(made);
|
|
38979
|
+
if (box)
|
|
38980
|
+
return buildMatrix(box.pos, undefined, box.size);
|
|
37178
38981
|
const matrix = render3DObjectMatrix(made).copy();
|
|
37179
38982
|
return bounds ? matrix.multiply(buildMatrix(bounds.min.add(bounds.max).scale(.5), undefined,
|
|
37180
38983
|
bounds.max.subtract(bounds.min))) : matrix.multiply(buildMatrix(vec3(), undefined, made.size3D));
|
|
@@ -37238,6 +39041,15 @@ function editor3DDraw()
|
|
|
37238
39041
|
editor3DDrawRing(editor3DTerrainHover.add(vec3(0, .05, 0)), EDITOR3D_AXES.x, EDITOR3D_AXES.z,
|
|
37239
39042
|
editor3DTerrainBrush.size / 2, hsl(0, 0, 1, .9), 2.5);
|
|
37240
39043
|
|
|
39044
|
+
// the box a box drag fills
|
|
39045
|
+
const region = map && editor3DDrag?.region;
|
|
39046
|
+
if (region)
|
|
39047
|
+
{
|
|
39048
|
+
const size = region.hi.subtract(region.lo).add(vec3(1));
|
|
39049
|
+
editor3DDrawWire(buildMatrix(map.pos3D.add(region.lo).add(size.scale(.5)), undefined, size.add(vec3(.02))),
|
|
39050
|
+
editor3DDrag.mode === 'place' ? hsl(0, 0, 1, .9) : editor3DDrag.mode === 'remove' ? hsl(0, 1, .6) :
|
|
39051
|
+
hsl(.15, 1, .6), 2.5);
|
|
39052
|
+
}
|
|
37241
39053
|
// the map's edges, to see where it ends
|
|
37242
39054
|
if (map)
|
|
37243
39055
|
editor3DDrawWire(buildMatrix(map.pos3D.add(map.mapSize.scale(.5)), undefined, map.mapSize),
|
|
@@ -37264,9 +39076,8 @@ function editor3DDraw()
|
|
|
37264
39076
|
else if (kind === 'plane')
|
|
37265
39077
|
{
|
|
37266
39078
|
// a flat square across its two axes, in the color of the axis it does not move along
|
|
37267
|
-
const size = vec3(tip * 1.6).subtract(handle.
|
|
37268
|
-
r.drawBox(points[0], size, lit ? color : EDITOR3D_AXIS_COLORS[
|
|
37269
|
-
.replace(axis[1], '')].scale(1, .7));
|
|
39079
|
+
const size = vec3(tip * 1.6).subtract(EDITOR3D_AXES[handle.across].scale(tip * 1.5));
|
|
39080
|
+
r.drawBox(points[0], size, lit ? color : EDITOR3D_AXIS_COLORS[handle.across].scale(1, .7), handle.turn);
|
|
37270
39081
|
}
|
|
37271
39082
|
else if (kind === 'scaleAll')
|
|
37272
39083
|
r.drawBox(center, tip * 1.4, lit ? color : WHITE);
|
|
@@ -37276,7 +39087,8 @@ function editor3DDraw()
|
|
|
37276
39087
|
r.drawLine(points[0], points[1], width, color);
|
|
37277
39088
|
const turn = axis === 'x' ? vec3(0, 0, -PI / 2) : axis === 'z' ? vec3(PI / 2, 0, 0) : undefined;
|
|
37278
39089
|
const at = points[1].add(handle.direction.scale(tip));
|
|
37279
|
-
|
|
39090
|
+
const cone = buildMatrix(vec3(), turn, vec3(tip * 1.4, tip * 2, tip * 1.4));
|
|
39091
|
+
r.drawMesh(editor3DConeMesh(), buildMatrix(at, handle.turn).multiply(cone), undefined, color);
|
|
37280
39092
|
}
|
|
37281
39093
|
else
|
|
37282
39094
|
{
|
|
@@ -37284,6 +39096,7 @@ function editor3DDraw()
|
|
|
37284
39096
|
r.drawBox(points[1], tip, color, editor3DRotation(editor3DSelected()[0]).scale(PI / 180));
|
|
37285
39097
|
}
|
|
37286
39098
|
}
|
|
39099
|
+
editor3DDrawGame();
|
|
37287
39100
|
}
|
|
37288
39101
|
|
|
37289
39102
|
// the editor's drawing on the 2D layer: the name of each marker, and the box being dragged out to select with
|
|
@@ -37374,9 +39187,11 @@ function editor3DPanelInit()
|
|
|
37374
39187
|
['scale', 'Scale', 'R'], ['blocks', 'Blocks', 'B'], ['terrain', 'Terrain', 'T']])
|
|
37375
39188
|
toolButtons[tool] = button(tools, text, ()=>
|
|
37376
39189
|
{
|
|
37377
|
-
|
|
39190
|
+
editor3DPickTool(tool);
|
|
37378
39191
|
(tool === 'select' || tool === 'blocks' || tool === 'terrain') && (editor3DBrush = undefined);
|
|
37379
39192
|
}, key);
|
|
39193
|
+
// the game's own tools, buttons and controls, under the editor's tools
|
|
39194
|
+
const game = editorElement('div', panel);
|
|
37380
39195
|
const snap = editorElement('div', panel, box);
|
|
37381
39196
|
const grid = check(snap, 'Grid snap', (on)=> editor3DGrid = on, 'G, and Ctrl flips it for a drag');
|
|
37382
39197
|
const steps = row(snap);
|
|
@@ -37385,12 +39200,26 @@ function editor3DPanelInit()
|
|
|
37385
39200
|
const scaleStep = choice(steps, 'Size', [1, .5, .25], (v)=> editor3DScaleStep = v);
|
|
37386
39201
|
const ground = check(snap, 'Ground snap', (on)=> editor3DGroundSnap = on,
|
|
37387
39202
|
'A drag of an object slides it along what is under the mouse');
|
|
39203
|
+
const ownAxes = check(snap, 'Own axes', (on)=> editor3DLocalAxes = on,
|
|
39204
|
+
'L: the Move handles go along the object\'s own axes, as it is turned, not the world\'s');
|
|
37388
39205
|
|
|
37389
39206
|
// the types, a click picks one up to place, made again when types are added
|
|
37390
39207
|
editorElement('div', panel, 'color:#aaa;margin-top:4px', 'Place');
|
|
37391
39208
|
const types = row();
|
|
39209
|
+
const prefabNote = editorElement('div', panel, 'color:#f86'); // why a prefab was not loaded
|
|
37392
39210
|
const properties = editorElement('div', panel, box);
|
|
37393
39211
|
|
|
39212
|
+
// prefabs: the selection made one, or the selected instance's own
|
|
39213
|
+
const prefabBox = editorElement('div', panel, box);
|
|
39214
|
+
|
|
39215
|
+
// the prefab that is open, and the way back
|
|
39216
|
+
const prefabOpen = editorElement('div', panel, 'padding:4px;margin:4px 0;background:#235;border-radius:3px;' +
|
|
39217
|
+
'display:flex;gap:6px;align-items:center');
|
|
39218
|
+
panel.insertBefore(prefabOpen, panel.firstChild);
|
|
39219
|
+
const prefabName = editorElement('span', prefabOpen, 'flex:1');
|
|
39220
|
+
button(prefabOpen, 'Back', ()=> editor3DPrefabBack(),
|
|
39221
|
+
'Backspace with nothing selected: back to where the prefab was opened from, every instance of it follows');
|
|
39222
|
+
|
|
37394
39223
|
// the Blocks tool's box: a map to add, or the types to paint with
|
|
37395
39224
|
const blocks = editorElement('div', panel, box);
|
|
37396
39225
|
|
|
@@ -37421,13 +39250,15 @@ function editor3DPanelInit()
|
|
|
37421
39250
|
|
|
37422
39251
|
const storage = editorElement('div', panel, 'color:#f86;margin-top:4px',
|
|
37423
39252
|
'Autosave failed, storage is full: Save to a file');
|
|
39253
|
+
const prefabUnsaved = editorElement('div', panel, 'color:#fc6;margin-top:4px');
|
|
37424
39254
|
const hint = editorElement('div', panel, 'color:#8ab;margin-top:4px');
|
|
37425
39255
|
const help = editorElement('div', panel, 'color:#aaa;margin-top:4px;border-top:1px solid #444;padding-top:4px');
|
|
37426
39256
|
for (const line of editor3DHelpLines)
|
|
37427
39257
|
editorElement('div', help, 'margin:2px 0', line);
|
|
39258
|
+
const gameHelp = editorElement('div', help);
|
|
37428
39259
|
button(help, 'Close', ()=> editor3DHelp = false, '?');
|
|
37429
39260
|
|
|
37430
|
-
editor3DPanelParts = {restart, pending, toolButtons, grid, moveStep, rotateStep, scaleStep, ground, types,
|
|
39261
|
+
editor3DPanelParts = {game, gameHelp, restart, pending, toolButtons, grid, moveStep, rotateStep, scaleStep, ground, ownAxes, types, prefabBox, prefabOpen, prefabName, prefabUnsaved, prefabNote,
|
|
37431
39262
|
properties, blocks, terrainBox, sceneOn, sceneRows, playFrom, storage, hint, help, typeNames: ''};
|
|
37432
39263
|
}
|
|
37433
39264
|
|
|
@@ -37442,49 +39273,126 @@ function editor3DPanelUpdate()
|
|
|
37442
39273
|
editor3DPanel || editor3DPanelInit();
|
|
37443
39274
|
editor3DPanel.style.display = '';
|
|
37444
39275
|
const p = editor3DPanelParts, lit = '2px solid #4af';
|
|
37445
|
-
p.
|
|
39276
|
+
editorGamePanel(p.game);
|
|
39277
|
+
editorGameHelp(p.gameHelp);
|
|
39278
|
+
p.restart.style.display = editorHas('onRestart') ? '' : 'none';
|
|
37446
39279
|
p.pending.style.display = editor3DRecords.get(editor3DLevel)?.pending ? '' : 'none';
|
|
37447
39280
|
for (const tool in p.toolButtons)
|
|
37448
39281
|
p.toolButtons[tool].style.outline = tool === editor3DTool ? lit : '';
|
|
37449
39282
|
p.grid.checked = editor3DGrid;
|
|
37450
39283
|
p.ground.checked = editor3DGroundSnap;
|
|
39284
|
+
p.ownAxes.checked = editor3DLocalAxes;
|
|
37451
39285
|
p.moveStep.value = editor3DMoveStep + '';
|
|
37452
39286
|
p.rotateStep.value = editor3DRotateStep + '';
|
|
37453
39287
|
p.scaleStep.value = editor3DScaleStep + '';
|
|
37454
39288
|
p.playFrom.checked = editor3DPlayFromMouse;
|
|
37455
|
-
p.playFrom.parentElement.style.display =
|
|
39289
|
+
p.playFrom.parentElement.style.display = editorHas('onPlayFrom') ? 'flex' : 'none';
|
|
37456
39290
|
p.storage.style.display = editor3DSaveFailed ? '' : 'none';
|
|
37457
39291
|
p.hint.textContent = editor3DHint();
|
|
37458
39292
|
p.help.style.display = editor3DHelp ? '' : 'none';
|
|
37459
39293
|
|
|
37460
39294
|
// a button for each type, made again when the types changed
|
|
37461
|
-
const names =
|
|
39295
|
+
const names = editor3DPlaceNames(), plain = names.filter((name)=> !level3DPrefabs.has(name));
|
|
39296
|
+
const prefabs = names.slice(plain.length);
|
|
39297
|
+
const typeNames = plain.join('\n') + '\n\n' + prefabs.join('\n');
|
|
37462
39298
|
if (p.typeNames !== typeNames)
|
|
37463
39299
|
{
|
|
37464
39300
|
p.typeNames = typeNames;
|
|
37465
|
-
p.
|
|
39301
|
+
p.typeButtons = names.map((name)=>
|
|
37466
39302
|
{
|
|
37467
39303
|
const b = editorElement('button', undefined, 'padding:3px 6px;cursor:pointer', name);
|
|
37468
39304
|
b.onclick = ()=> { editor3DBrush = editor3DBrush === name ? undefined : name; b.blur(); };
|
|
37469
39305
|
return b;
|
|
37470
|
-
})
|
|
37471
|
-
|
|
37472
|
-
|
|
39306
|
+
});
|
|
39307
|
+
const label = editorElement('span', undefined, 'color:#aaa;width:100%', 'Prefabs');
|
|
39308
|
+
const load = editorElement('button', undefined, 'padding:3px 6px;cursor:pointer', 'Load prefab…');
|
|
39309
|
+
load.title = 'Add a prefab from a file the editor saved, by its file name, to place in this level';
|
|
39310
|
+
load.onclick = ()=> { editor3DPrefabPickFile(); load.blur(); };
|
|
39311
|
+
p.types.replaceChildren(...p.typeButtons.slice(0, plain.length), label, ...p.typeButtons.slice(plain.length), load);
|
|
39312
|
+
}
|
|
39313
|
+
p.prefabNote.style.display = editor3DPrefabMessage && !editor3DSelection.size ? '' : 'none';
|
|
39314
|
+
p.prefabNote.textContent = editor3DPrefabMessage;
|
|
39315
|
+
names.forEach((name, i)=> p.typeButtons[i].style.outline = name === editor3DBrush ? lit : '');
|
|
37473
39316
|
editor3DPropertiesUpdate(p.properties);
|
|
39317
|
+
editor3DPrefabBoxUpdate(p.prefabBox);
|
|
39318
|
+
p.prefabOpen.style.display = editor3DPrefabStack.length ? 'flex' : 'none';
|
|
39319
|
+
p.prefabName.textContent = 'Editing prefab ' + editor3DPrefabStack.map((frame)=> frame.name).join(' > ');
|
|
39320
|
+
p.prefabUnsaved.style.display = editor3DPrefabDirty.size ? '' : 'none';
|
|
39321
|
+
p.prefabUnsaved.textContent = 'Edited and not in its file: ' + [...editor3DPrefabDirty].join(', ') +
|
|
39322
|
+
'. Open it with Enter and Save';
|
|
37474
39323
|
editor3DBlocksUpdate(p.blocks);
|
|
37475
39324
|
editor3DTerrainBoxUpdate(p.terrainBox);
|
|
37476
39325
|
p.sceneOn.checked = !!editor3DScene();
|
|
37477
39326
|
editor3DSceneUpdate(p.sceneRows);
|
|
37478
39327
|
}
|
|
37479
39328
|
|
|
39329
|
+
// the prefab box, shown with a selection: a name and a button to make it a prefab, and for one selected instance
|
|
39330
|
+
// its prefab's name, Unpack, Export, and for a prefab of the level's own whether it is attached
|
|
39331
|
+
function editor3DPrefabBoxUpdate(box)
|
|
39332
|
+
{
|
|
39333
|
+
const selected = editor3DSelected(), made = selected.length === 1 && editor3DInstances.get(selected[0].id);
|
|
39334
|
+
const instance = made instanceof Prefab3D ? made : undefined, prefab = instance && level3DPrefabs.get(instance.prefabName);
|
|
39335
|
+
const key = !selected.length ? '' : [selected.length, instance?.prefabName, prefab?.attached, prefab?.fromLevel,
|
|
39336
|
+
editor3DPrefabMessage, editor3DPrefabStack.length] + '';
|
|
39337
|
+
box.style.display = key ? '' : 'none';
|
|
39338
|
+
if (box.dataset.key === key || box.contains(document.activeElement)) return;
|
|
39339
|
+
box.dataset.key = key;
|
|
39340
|
+
box.replaceChildren();
|
|
39341
|
+
if (!key) return;
|
|
39342
|
+
const field = 'background:#222;color:#eee', press = 'padding:2px 6px;cursor:pointer';
|
|
39343
|
+
const row = ()=> editorElement('div', box, 'display:flex;gap:4px;align-items:center;margin:2px 0;flex-wrap:wrap');
|
|
39344
|
+
if (prefab)
|
|
39345
|
+
{
|
|
39346
|
+
const line = row();
|
|
39347
|
+
editorElement('span', line, 'flex:1;color:#aaa', 'Prefab ' + instance.prefabName);
|
|
39348
|
+
const open = editorElement('button', line, press, 'Edit');
|
|
39349
|
+
open.title = 'Enter: open the prefab alone to edit it, every instance of it follows';
|
|
39350
|
+
open.onclick = ()=> { editor3DPrefabEnter(); open.blur(); };
|
|
39351
|
+
const unpack = editorElement('button', line, press, 'Unpack');
|
|
39352
|
+
unpack.title = 'Ctrl+Shift+G: turn this instance into the objects it is made of';
|
|
39353
|
+
unpack.onclick = ()=> { editor3DUnpack(); unpack.blur(); };
|
|
39354
|
+
const save = editorElement('button', line, press, 'Export');
|
|
39355
|
+
save.title = 'Save the prefab as a file of its own, to load with level3DLoadPrefab';
|
|
39356
|
+
save.onclick = ()=>
|
|
39357
|
+
{
|
|
39358
|
+
saveText(editor3DPrefabJSON(instance.prefabName), instance.prefabName + '.json', 'application/json');
|
|
39359
|
+
save.blur();
|
|
39360
|
+
};
|
|
39361
|
+
if (prefab.fromLevel && !editor3DPrefabStack.length)
|
|
39362
|
+
{
|
|
39363
|
+
const label = editorElement('label', box, 'display:flex;gap:6px;align-items:center;margin:2px 0');
|
|
39364
|
+
label.title = 'Its parts are attached to each instance and move with it as one body, with no ' +
|
|
39365
|
+
'collision of their own; off, each part is an object of its own and solid';
|
|
39366
|
+
const attached = editorElement('input', label);
|
|
39367
|
+
attached.type = 'checkbox';
|
|
39368
|
+
attached.checked = prefab.attached;
|
|
39369
|
+
attached.onchange = ()=> { editor3DPrefabSetAttached(instance.prefabName, attached.checked); attached.blur(); };
|
|
39370
|
+
editorElement('span', label, '', 'Attached, one body');
|
|
39371
|
+
}
|
|
39372
|
+
}
|
|
39373
|
+
const line = row();
|
|
39374
|
+
const name = editorElement('input', line, field + ';flex:1;min-width:60px');
|
|
39375
|
+
name.placeholder = 'prefab name';
|
|
39376
|
+
const make = editorElement('button', line, press, 'Make prefab');
|
|
39377
|
+
make.title = 'Ctrl+G: the selection becomes one thing to place many times, kept in the level';
|
|
39378
|
+
make.onclick = ()=> { editor3DMakePrefab(name.value); make.blur(); };
|
|
39379
|
+
name.onkeydown = (e)=> { e.key === 'Enter' && (editor3DMakePrefab(name.value), name.blur()); };
|
|
39380
|
+
editor3DPrefabMessage && editorElement('div', box, 'color:#f86', editor3DPrefabMessage);
|
|
39381
|
+
}
|
|
39382
|
+
|
|
39383
|
+
// a level's color as a color input takes it, six digits and no alpha, white for one it can not show
|
|
39384
|
+
function editor3DInputColor(hex) { return /^#[0-9a-f]{6}/i.test(hex) ? hex.slice(0, 7) : '#ffffff'; }
|
|
39385
|
+
|
|
37480
39386
|
// the terrain box, shown with the Terrain tool: without a terrain, its size, cells and height and a button to add
|
|
37481
|
-
// it; with one, the brush
|
|
39387
|
+
// it; with one, the brush, its size and strength or the color it paints, the terrain's color, and a button to
|
|
39388
|
+
// take it away
|
|
37482
39389
|
function editor3DTerrainBoxUpdate(box)
|
|
37483
39390
|
{
|
|
37484
39391
|
const map = editor3DTerrainMap(), shown = editor3DTool === 'terrain';
|
|
37485
39392
|
box.style.display = shown ? '' : 'none';
|
|
37486
39393
|
if (!shown) return;
|
|
37487
|
-
const terrain = editor3DLevelPart('terrain'),
|
|
39394
|
+
const terrain = editor3DLevelPart('terrain'), brush = editor3DTerrainBrush;
|
|
39395
|
+
const key = map ? ['terrain', terrain.color, brush.mode] + '' : 'none';
|
|
37488
39396
|
if (box.dataset.key === key || box.contains(document.activeElement)) return;
|
|
37489
39397
|
box.dataset.key = key;
|
|
37490
39398
|
box.replaceChildren();
|
|
@@ -37532,13 +39440,27 @@ function editor3DTerrainBoxUpdate(box)
|
|
|
37532
39440
|
input.oninput = ()=> set(parseFloat(input.value));
|
|
37533
39441
|
input.onchange = ()=> input.blur();
|
|
37534
39442
|
};
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
(
|
|
37539
|
-
|
|
39443
|
+
const brushes = editorElement('select', line('Brush', 'X: sculpt raises and lowers the ground, flatten brings ' +
|
|
39444
|
+
'it to the height pressed, paint colors it'), field + ';width:120px');
|
|
39445
|
+
for (const name of editor3DTerrainBrushes)
|
|
39446
|
+
editorElement('option', brushes, '', name[0].toUpperCase() + name.slice(1)).value = name;
|
|
39447
|
+
brushes.value = brush.mode;
|
|
39448
|
+
brushes.onchange = ()=> { brush.mode = brushes.value; brushes.blur(); };
|
|
39449
|
+
slider('Brush size', 'How far across the brush is', 1, 32, .5, brush.size, (v)=> brush.size = v);
|
|
39450
|
+
if (brush.mode === 'paint')
|
|
39451
|
+
{
|
|
39452
|
+
const paint = editorElement('input', line('Paint', 'The color the brush paints'), field + ';width:60px');
|
|
39453
|
+
paint.type = 'color';
|
|
39454
|
+
paint.value = editor3DInputColor(brush.color);
|
|
39455
|
+
paint.oninput = ()=> brush.color = paint.value;
|
|
39456
|
+
paint.onchange = ()=> { brush.color = paint.value; paint.blur(); };
|
|
39457
|
+
}
|
|
39458
|
+
else
|
|
39459
|
+
slider('Strength', 'How fast the brush works', .05, 1, .05, brush.strength, (v)=> brush.strength = v);
|
|
39460
|
+
const color = editorElement('input', line('Color', 'The color of the ground that is not painted'),
|
|
39461
|
+
field + ';width:60px');
|
|
37540
39462
|
color.type = 'color';
|
|
37541
|
-
color.value =
|
|
39463
|
+
color.value = editor3DInputColor(terrain.color);
|
|
37542
39464
|
color.oninput = ()=> editor3DChangePart('terrain', (t)=> ({...t, color: color.value}));
|
|
37543
39465
|
color.onchange = ()=> { color.oninput(); editor3DStrokeEnd(); color.blur(); };
|
|
37544
39466
|
const remove = editorElement('button', box, 'margin-top:4px;padding:2px 6px;cursor:pointer', 'Remove terrain');
|
|
@@ -37554,7 +39476,8 @@ function editor3DBlocksUpdate(box)
|
|
|
37554
39476
|
box.style.display = shown ? '' : 'none';
|
|
37555
39477
|
if (!shown) return;
|
|
37556
39478
|
const first = map?.tileInfo, texture = first?.textureInfo;
|
|
37557
|
-
const key = map ? ['map', editor3DBlockType, texture?.size.x, map.blockTypes.length
|
|
39479
|
+
const key = map ? ['map', editor3DBlockType, texture?.size.x, map.blockTypes.length, editor3DBlockBox,
|
|
39480
|
+
map.mapSize].join() : 'none';
|
|
37558
39481
|
if (box.dataset.key === key || box.contains(document.activeElement)) return;
|
|
37559
39482
|
box.dataset.key = key;
|
|
37560
39483
|
box.replaceChildren();
|
|
@@ -37575,7 +39498,7 @@ function editor3DBlocksUpdate(box)
|
|
|
37575
39498
|
add.title = 'An empty map of blocks, centered, its bottom on the ground';
|
|
37576
39499
|
add.onclick = ()=>
|
|
37577
39500
|
{
|
|
37578
|
-
const [x, y, z] = inputs.map((i)=> clamp(floor(parseFloat(i.value)) ||
|
|
39501
|
+
const [x, y, z] = inputs.map((i, k)=> clamp(floor(parseFloat(i.value)) || [32, 16, 32][k], 1, 256));
|
|
37579
39502
|
editor3DVoxelAdd(vec3(x, y, z));
|
|
37580
39503
|
add.blur();
|
|
37581
39504
|
};
|
|
@@ -37615,6 +39538,46 @@ function editor3DBlocksUpdate(box)
|
|
|
37615
39538
|
}
|
|
37616
39539
|
catch {} // an image that can not be drawn or read, the title still says the type
|
|
37617
39540
|
}
|
|
39541
|
+
// box fill: a drag fills the rectangle dragged, this many layers tall
|
|
39542
|
+
const boxRow = editorElement('label', box, 'display:flex;gap:6px;align-items:center;margin:4px 0 2px');
|
|
39543
|
+
boxRow.title = 'X: a drag fills the whole rectangle from the press to the mouse, Shift clears it, Ctrl repaints';
|
|
39544
|
+
const boxOn = editorElement('input', boxRow);
|
|
39545
|
+
boxOn.type = 'checkbox';
|
|
39546
|
+
boxOn.checked = editor3DBlockBox;
|
|
39547
|
+
boxOn.onchange = ()=> { editor3DBlockBox = boxOn.checked; boxOn.blur(); };
|
|
39548
|
+
editorElement('span', boxRow, 'flex:1', 'Box fill, height');
|
|
39549
|
+
const boxHeight = editorElement('input', boxRow, field);
|
|
39550
|
+
boxHeight.type = 'number';
|
|
39551
|
+
boxHeight.min = '1', boxHeight.max = '256', boxHeight.step = '1';
|
|
39552
|
+
boxHeight.value = editor3DBlockBoxHeight + '';
|
|
39553
|
+
boxHeight.onchange = ()=>
|
|
39554
|
+
{
|
|
39555
|
+
editor3DBlockBoxHeight = clamp(floor(parseFloat(boxHeight.value)) || 1, 1, 256);
|
|
39556
|
+
boxHeight.value = editor3DBlockBoxHeight + '';
|
|
39557
|
+
boxHeight.blur();
|
|
39558
|
+
};
|
|
39559
|
+
|
|
39560
|
+
// the map's size, to change
|
|
39561
|
+
const sizeRow = editorElement('label', box, 'display:flex;gap:4px;align-items:center;margin:2px 0');
|
|
39562
|
+
editorElement('span', sizeRow, 'flex:1', 'Map size');
|
|
39563
|
+
const axes = ['x', 'y', 'z'], sizes = axes.map((axis)=>
|
|
39564
|
+
{
|
|
39565
|
+
const input = editorElement('input', sizeRow, field);
|
|
39566
|
+
input.type = 'number';
|
|
39567
|
+
input.min = '1', input.max = '256', input.step = '1';
|
|
39568
|
+
input.value = map.mapSize[axis] + '';
|
|
39569
|
+
return input;
|
|
39570
|
+
});
|
|
39571
|
+
const resize = editorElement('button', box, 'padding:2px 6px;cursor:pointer;margin-right:4px', 'Resize');
|
|
39572
|
+
resize.title = 'Change the map\'s size, its blocks stay where they are; Undo brings back what was cut off';
|
|
39573
|
+
resize.onclick = ()=>
|
|
39574
|
+
{
|
|
39575
|
+
// a field left empty keeps the size it has
|
|
39576
|
+
const [x, y, z] = sizes.map((i, k)=> clamp(floor(parseFloat(i.value)) || map.mapSize[axes[k]], 1, 256));
|
|
39577
|
+
editor3DVoxelResize(vec3(x, y, z));
|
|
39578
|
+
resize.blur();
|
|
39579
|
+
};
|
|
39580
|
+
|
|
37618
39581
|
const remove = editorElement('button', box, 'margin-top:4px;padding:2px 6px;cursor:pointer', 'Remove map');
|
|
37619
39582
|
remove.title = 'Take the block map out of the level, Undo brings it back';
|
|
37620
39583
|
remove.onclick = ()=> { editor3DVoxelRemove(); remove.blur(); };
|
|
@@ -37633,7 +39596,11 @@ function editor3DSceneUpdate(box)
|
|
|
37633
39596
|
|
|
37634
39597
|
// a setting the block leaves out shows as it is on screen
|
|
37635
39598
|
const view = editor3DSceneFromView(), value = (name)=> scene[name] ?? view[name];
|
|
37636
|
-
|
|
39599
|
+
// an input changes the scene as it is when it is used, not as it was when the box was made: the box is not
|
|
39600
|
+
// made again while it has the focus, so a field edited right after another must not put the other one back;
|
|
39601
|
+
// set is given the setting's value now, or what shows for one the block leaves out
|
|
39602
|
+
const change = (name, set)=> editor3DChangeScene((s={})=>
|
|
39603
|
+
({...s, [name]: typeof set === 'function' ? set(s[name] ?? view[name]) : set}));
|
|
37637
39604
|
const field = 'background:#222;color:#eee';
|
|
37638
39605
|
const line = (name, title='')=>
|
|
37639
39606
|
{
|
|
@@ -37661,7 +39628,7 @@ function editor3DSceneUpdate(box)
|
|
|
37661
39628
|
{
|
|
37662
39629
|
const input = editorElement('input', line(name, title), field + ';width:60px');
|
|
37663
39630
|
input.type = 'color';
|
|
37664
|
-
input.value =
|
|
39631
|
+
input.value = editor3DInputColor(hex);
|
|
37665
39632
|
return live(input, ()=> set(input.value));
|
|
37666
39633
|
};
|
|
37667
39634
|
const number = (row, v, set)=>
|
|
@@ -37674,7 +39641,7 @@ function editor3DSceneUpdate(box)
|
|
|
37674
39641
|
input.onchange = ()=>
|
|
37675
39642
|
{
|
|
37676
39643
|
const n = parseFloat(input.value);
|
|
37677
|
-
isNumber(n) && n >= 0
|
|
39644
|
+
isNumber(n) && n >= 0 ? set(v = n) : input.value = v + ''; // one not taken shows what it has again
|
|
37678
39645
|
editor3DStrokeEnd();
|
|
37679
39646
|
input.blur();
|
|
37680
39647
|
};
|
|
@@ -37699,7 +39666,7 @@ function editor3DSceneUpdate(box)
|
|
|
37699
39666
|
if (sky)
|
|
37700
39667
|
{
|
|
37701
39668
|
['Sky top', 'Sky horizon', 'Sky bottom'].forEach((name, i)=> color(name, '', sky[i], (hex)=>
|
|
37702
|
-
change('sky', sky.map((c, k)=> k === i ? hex : c))));
|
|
39669
|
+
change('sky', (now)=> (isArray(now) && now.length === 3 ? now : sky).map((c, k)=> k === i ? hex : c))));
|
|
37703
39670
|
slider('Ambient', 'How much of the sky colors lights the scene', 0, 1, .05,
|
|
37704
39671
|
isNumber(scene.ambient) ? scene.ambient : .5, (v)=> change('ambient', v));
|
|
37705
39672
|
}
|
|
@@ -37707,16 +39674,17 @@ function editor3DSceneUpdate(box)
|
|
|
37707
39674
|
// the sun, as two angles
|
|
37708
39675
|
const [around, height] = editor3DSunAngles(value('sunDirection'));
|
|
37709
39676
|
slider('Sun around', 'Where the sun is around the scene, in degrees', 0, 359, 1, around, (v)=>
|
|
37710
|
-
change('sunDirection', editor3DSunDirection(v, editor3DSunAngles(
|
|
37711
|
-
slider('Sun height', 'How high the sun is over the horizon, in degrees', 5,
|
|
37712
|
-
change('sunDirection', editor3DSunDirection(editor3DSunAngles(
|
|
39677
|
+
change('sunDirection', (now)=> editor3DSunDirection(v, editor3DSunAngles(now)[1])));
|
|
39678
|
+
slider('Sun height', 'How high the sun is over the horizon, in degrees', 5, 89, 1, height, (v)=>
|
|
39679
|
+
change('sunDirection', (now)=> editor3DSunDirection(editor3DSunAngles(now)[0], v)));
|
|
37713
39680
|
color('Sun color', '', value('sunColor'), (hex)=> change('sunColor', hex));
|
|
37714
39681
|
|
|
37715
39682
|
// the fog, by its distances, an end of 0 for none
|
|
37716
39683
|
const fog = isArray(value('fog')) ? value('fog') : [0, 0], fogRow = line('Fog start, end',
|
|
37717
39684
|
'Where the fog starts and where it is total, an end of 0 for no fog');
|
|
37718
|
-
|
|
37719
|
-
number(fogRow, fog[
|
|
39685
|
+
const fogNow = (now)=> isArray(now) && now.length === 2 ? now : fog;
|
|
39686
|
+
number(fogRow, fog[0], (v)=> change('fog', (now)=> [v, fogNow(now)[1]]));
|
|
39687
|
+
number(fogRow, fog[1], (v)=> change('fog', (now)=> [fogNow(now)[0], v]));
|
|
37720
39688
|
color('Fog color', 'The sky\'s horizon color when the level has a sky and no fog color',
|
|
37721
39689
|
scene.fogColor ?? (sky ? sky[1] : view.fogColor), (hex)=> change('fogColor', hex));
|
|
37722
39690
|
toggle('Shadows', 'The sun casts shadows', !!value('shadows'), (on)=> change('shadows', on));
|
|
@@ -37896,6 +39864,8 @@ export
|
|
|
37896
39864
|
debugKeysAlways,
|
|
37897
39865
|
debugTweakables,
|
|
37898
39866
|
levelEditor,
|
|
39867
|
+
LevelEditor,
|
|
39868
|
+
setLevelEditor,
|
|
37899
39869
|
|
|
37900
39870
|
// Debug
|
|
37901
39871
|
ASSERT,
|
|
@@ -37957,6 +39927,7 @@ export
|
|
|
37957
39927
|
touchInputEnable,
|
|
37958
39928
|
touchGamepadEnable,
|
|
37959
39929
|
touchGamepadPassthrough,
|
|
39930
|
+
touchPinchWheel,
|
|
37960
39931
|
touchGamepadCenterButtonSize,
|
|
37961
39932
|
touchGamepadButtonCount,
|
|
37962
39933
|
touchGamepadLeftStick,
|
|
@@ -38018,6 +39989,7 @@ export
|
|
|
38018
39989
|
setInputWASDEmulateDirection,
|
|
38019
39990
|
setTouchGamepadEnable,
|
|
38020
39991
|
setTouchGamepadPassthrough,
|
|
39992
|
+
setTouchPinchWheel,
|
|
38021
39993
|
setTouchGamepadCenterButtonSize,
|
|
38022
39994
|
setTouchGamepadButtonCount,
|
|
38023
39995
|
setTouchGamepadLeftStick,
|
|
@@ -38221,6 +40193,7 @@ export
|
|
|
38221
40193
|
mouseDelta,
|
|
38222
40194
|
mouseDeltaScreen,
|
|
38223
40195
|
mouseWheel,
|
|
40196
|
+
touchPinch,
|
|
38224
40197
|
mouseInWindow,
|
|
38225
40198
|
isUsingGamepad,
|
|
38226
40199
|
lastInputDevice,
|
|
@@ -38404,6 +40377,10 @@ export
|
|
|
38404
40377
|
setScene,
|
|
38405
40378
|
getScene,
|
|
38406
40379
|
|
|
40380
|
+
// Parallax
|
|
40381
|
+
ParallaxLayer,
|
|
40382
|
+
parallaxMountains,
|
|
40383
|
+
|
|
38407
40384
|
// Path Finding
|
|
38408
40385
|
PathFinder,
|
|
38409
40386
|
PathFinderNode,
|
|
@@ -38484,6 +40461,10 @@ export
|
|
|
38484
40461
|
level3DAddType,
|
|
38485
40462
|
level3DAddMesh,
|
|
38486
40463
|
level3DLoad,
|
|
40464
|
+
level3DAddPrefab,
|
|
40465
|
+
level3DLoadPrefab,
|
|
40466
|
+
level3DSpawn,
|
|
40467
|
+
Prefab3D,
|
|
38487
40468
|
level3DVoxelSetup,
|
|
38488
40469
|
|
|
38489
40470
|
// Three.js
|