littlejsengine 1.24.0 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -1
- package/README.md +1 -1
- package/REFERENCE.md +7 -4
- package/dist/littlejs.d.ts +29 -18
- package/dist/littlejs.esm.js +117 -53
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +117 -53
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +96 -45
- package/package.json +1 -1
- package/plugins/box2d.js +1 -1
- package/plugins/render3d.js +3 -2
- package/plugins/render3dEditor.js +4 -2
- package/plugins/render3dExtras.js +9 -6
- package/plugins/render3dVoxels.js +15 -8
- package/src/engine.js +1 -1
- package/src/engineEditor.js +17 -6
- package/src/engineSettings.js +5 -1
- package/src/engineTileLayer.js +62 -26
package/dist/littlejs.release.js
CHANGED
|
@@ -35,7 +35,7 @@ const engineName = 'LittleJS';
|
|
|
35
35
|
* @type {string}
|
|
36
36
|
* @default
|
|
37
37
|
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.24.
|
|
38
|
+
const engineVersion = '1.24.1';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -3211,7 +3211,11 @@ function setGLEnable(enable)
|
|
|
3211
3211
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
3212
3212
|
* @param {number} sides
|
|
3213
3213
|
* @memberof Settings */
|
|
3214
|
-
function setGLCircleSides(sides)
|
|
3214
|
+
function setGLCircleSides(sides)
|
|
3215
|
+
{
|
|
3216
|
+
false&&ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
|
|
3217
|
+
glCircleSides = sides;
|
|
3218
|
+
}
|
|
3215
3219
|
|
|
3216
3220
|
/** Set default size of tiles in pixels
|
|
3217
3221
|
* @param {Vector2} size
|
|
@@ -8733,7 +8737,7 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
8733
8737
|
* @param {Object} tileMapData - Level data from exported data
|
|
8734
8738
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
|
|
8735
8739
|
* @param {number} [renderOrder] - Render order of the top layer
|
|
8736
|
-
* @param {number}
|
|
8740
|
+
* @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
|
|
8737
8741
|
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
8738
8742
|
* @return {Array<TileCollisionLayer>}
|
|
8739
8743
|
* @memberof TileLayers */
|
|
@@ -8765,7 +8769,8 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
8765
8769
|
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
8766
8770
|
const width = tileInfo.textureInfo?.size.x || 0;
|
|
8767
8771
|
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
8768
|
-
|
|
8772
|
+
const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
|
|
8773
|
+
tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
8769
8774
|
}
|
|
8770
8775
|
|
|
8771
8776
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
@@ -8796,6 +8801,14 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
8796
8801
|
};
|
|
8797
8802
|
addLayers(tileMapData.layers, WHITE, true);
|
|
8798
8803
|
|
|
8804
|
+
// a layer named for collision is the first tile layer of that name, which stays the same when layers are added
|
|
8805
|
+
if (typeof collisionLayer === 'string')
|
|
8806
|
+
{
|
|
8807
|
+
const name = collisionLayer;
|
|
8808
|
+
collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
|
|
8809
|
+
false&&ASSERT(collisionLayer >= 0, 'no tile layer named ' + name);
|
|
8810
|
+
}
|
|
8811
|
+
|
|
8799
8812
|
// create tile layers and fill with data
|
|
8800
8813
|
const tileLayers = [];
|
|
8801
8814
|
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
@@ -8851,31 +8864,38 @@ const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: '
|
|
|
8851
8864
|
|
|
8852
8865
|
/**
|
|
8853
8866
|
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
8854
|
-
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled
|
|
8855
|
-
*
|
|
8856
|
-
*
|
|
8857
|
-
*
|
|
8858
|
-
* - An IntGrid layer
|
|
8867
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
|
|
8868
|
+
* tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
|
|
8869
|
+
* (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
|
|
8870
|
+
* layer's, named with it, like Ground .25
|
|
8871
|
+
* - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
|
|
8872
|
+
* make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
|
|
8873
|
+
* the stacked and see-through layers change the indices from level to level:
|
|
8874
|
+
* tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
|
|
8859
8875
|
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
8860
8876
|
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
8861
8877
|
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
8862
8878
|
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
8863
|
-
* another grid size or another tileset is left out, with a warning in debug builds
|
|
8879
|
+
* another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
|
|
8880
|
+
* keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
|
|
8881
|
+
* - Layer offsets are not read
|
|
8864
8882
|
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
8865
8883
|
* @param {Object} ldtk - The LDtk project, its JSON
|
|
8866
8884
|
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
8867
8885
|
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
8868
8886
|
* @example
|
|
8869
8887
|
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
8870
|
-
* const layers = tileLayersLoad(map, tile(0, 16), 0,
|
|
8888
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
|
|
8871
8889
|
* objectLayersLoad(map);
|
|
8872
8890
|
* @memberof TileLayers */
|
|
8873
8891
|
function tileLayersFromLDtk(ldtk, level=0)
|
|
8874
8892
|
{
|
|
8875
|
-
const levels = ldtk?.levels || [];
|
|
8893
|
+
const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
|
|
8876
8894
|
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
8877
|
-
|
|
8878
|
-
|
|
8895
|
+
if (!data)
|
|
8896
|
+
throw new Error('tileLayersFromLDtk: no level ' + level);
|
|
8897
|
+
if (!isArray(data.layerInstances))
|
|
8898
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
|
|
8879
8899
|
const instances = data.layerInstances;
|
|
8880
8900
|
const tiled = (l)=> l.__type !== 'Entities';
|
|
8881
8901
|
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
@@ -8915,33 +8935,53 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
8915
8935
|
continue;
|
|
8916
8936
|
}
|
|
8917
8937
|
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
|
|
8918
|
-
const
|
|
8919
|
-
|
|
8920
|
-
if (!
|
|
8938
|
+
const ownTiles = !tiles.length || !tileset ||
|
|
8939
|
+
instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
|
|
8940
|
+
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
|
|
8921
8941
|
{
|
|
8922
8942
|
debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
|
|
8923
8943
|
continue;
|
|
8924
8944
|
}
|
|
8925
|
-
|
|
8926
|
-
|
|
8945
|
+
// an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
|
|
8946
|
+
const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
|
|
8947
|
+
const values = instance.__type === 'IntGrid';
|
|
8948
|
+
if (values)
|
|
8949
|
+
{
|
|
8950
|
+
const data = empty();
|
|
8951
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
|
|
8952
|
+
map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
|
|
8953
|
+
if (!tiles.length) continue;
|
|
8954
|
+
}
|
|
8955
|
+
|
|
8956
|
+
// a layer for each depth in a cell and each opacity, the first tiles drawn solid first
|
|
8957
|
+
const tilesName = values ? name + ' tiles' : name;
|
|
8958
|
+
const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
|
|
8927
8959
|
{
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8960
|
+
let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
|
|
8961
|
+
s || stacks.push(s = {depth, alpha, data: empty()});
|
|
8962
|
+
return s.data;
|
|
8963
|
+
};
|
|
8964
|
+
{
|
|
8965
|
+
// a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
|
|
8966
|
+
// Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
|
|
8967
|
+
// cell is the bottom one and each after it goes a layer higher
|
|
8931
8968
|
const depth = new Uint16Array(width * height);
|
|
8932
8969
|
for (const t of tiles)
|
|
8933
8970
|
{
|
|
8934
8971
|
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
8935
8972
|
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
8936
|
-
(
|
|
8973
|
+
stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
|
|
8937
8974
|
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
8938
8975
|
}
|
|
8939
8976
|
}
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
const
|
|
8943
|
-
|
|
8944
|
-
|
|
8977
|
+
const visible = instance.visible !== false;
|
|
8978
|
+
stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
|
|
8979
|
+
for (const {depth, alpha, data} of stacks)
|
|
8980
|
+
{
|
|
8981
|
+
const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
|
|
8982
|
+
map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
|
|
8983
|
+
width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
|
|
8984
|
+
}
|
|
8945
8985
|
}
|
|
8946
8986
|
return map;
|
|
8947
8987
|
}
|
|
@@ -16502,7 +16542,7 @@ class Box2dObject extends EngineObject
|
|
|
16502
16542
|
|
|
16503
16543
|
const points = [];
|
|
16504
16544
|
const radius = diameter/2;
|
|
16505
|
-
for (let i=sides; i
|
|
16545
|
+
for (let i=sides; i-- > 0;) // a count that is not a whole number still ends
|
|
16506
16546
|
points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
|
|
16507
16547
|
return this.addPoly(points, density, friction, restitution, isSensor);
|
|
16508
16548
|
}
|
|
@@ -26970,10 +27010,11 @@ class Light3D extends EngineObject3D
|
|
|
26970
27010
|
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
26971
27011
|
* @param {number} [intensity] - How bright it is
|
|
26972
27012
|
* @param {number} [saturation] - How colorful the ghosts are
|
|
27013
|
+
* @param {Color} [color] - Tints the flare, with the light's own color
|
|
26973
27014
|
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
26974
|
-
addFlare(size, count, intensity, saturation)
|
|
27015
|
+
addFlare(size, count, intensity, saturation, color)
|
|
26975
27016
|
{
|
|
26976
|
-
const flare = new LensFlare3D(size, count, intensity, saturation);
|
|
27017
|
+
const flare = new LensFlare3D(size, count, intensity, saturation, color);
|
|
26977
27018
|
this.flare = flare;
|
|
26978
27019
|
return flare;
|
|
26979
27020
|
}
|
|
@@ -28621,7 +28662,7 @@ function render3DFlareTile(shape)
|
|
|
28621
28662
|
* with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
|
|
28622
28663
|
* give it elements of your own, which may be tiles of the game's
|
|
28623
28664
|
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read; it is 0 while the
|
|
28624
|
-
* sun is off the screen, where nothing is tested
|
|
28665
|
+
* sun is too far off the screen for the flare to show, a third of the screen past its edge, where nothing is tested
|
|
28625
28666
|
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
28626
28667
|
* through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
|
|
28627
28668
|
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
@@ -28668,7 +28709,8 @@ class LensFlare3D extends EngineObject3D
|
|
|
28668
28709
|
this.elements = undefined;
|
|
28669
28710
|
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
28670
28711
|
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
28671
|
-
* in front of the light,
|
|
28712
|
+
* in front of the light but its lamp, a mesh around it no wider than half its radius, and a spotlight's
|
|
28713
|
+
* shows from inside its beam only; a DirectionalLight3D's is far
|
|
28672
28714
|
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
28673
28715
|
* sets this
|
|
28674
28716
|
* @type {Light3D|undefined} */
|
|
@@ -28808,13 +28850,15 @@ class LensFlare3D extends EngineObject3D
|
|
|
28808
28850
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
28809
28851
|
return true;
|
|
28810
28852
|
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
28811
|
-
// is in
|
|
28853
|
+
// is in, and no wider than half the light's reach, since a room or a whole level has a box around its lights
|
|
28854
|
+
// too, and its walls do hide them from outside
|
|
28812
28855
|
const lamp = (o)=>
|
|
28813
28856
|
{
|
|
28814
28857
|
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
28815
|
-
const matrix = render3DObjectMatrix(o);
|
|
28816
|
-
|
|
28817
|
-
|
|
28858
|
+
const matrix = render3DObjectMatrix(o), b = o.mesh.bounds || o.mesh.getBounds();
|
|
28859
|
+
const side = max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z) * render3DMaxStretch(matrix.m);
|
|
28860
|
+
if (!matrix.determinant() || side > this.light.radius / 2) return false;
|
|
28861
|
+
const p = matrix.copy().invert().transformPoint(source.pos);
|
|
28818
28862
|
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
28819
28863
|
p.z >= b.min.z && p.z <= b.max.z;
|
|
28820
28864
|
};
|
|
@@ -29451,7 +29495,7 @@ class VoxelMap extends EngineObject3D
|
|
|
29451
29495
|
/** @type {Array<Vector3>} */
|
|
29452
29496
|
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
29453
29497
|
this.chunksChanged = new Set;
|
|
29454
|
-
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
|
|
29498
|
+
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
|
|
29455
29499
|
this.blockTypes = [];
|
|
29456
29500
|
this.tiles = new Map; // tile index to the uvs of its corners
|
|
29457
29501
|
|
|
@@ -29523,27 +29567,29 @@ class VoxelMap extends EngineObject3D
|
|
|
29523
29567
|
* @param {number} type - 1 to 255
|
|
29524
29568
|
* @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
|
|
29525
29569
|
* six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
|
|
29526
|
-
* @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes
|
|
29527
|
-
* leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
29528
|
-
* transparent stage, and see-through too
|
|
29529
|
-
|
|
29570
|
+
* @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
|
|
29571
|
+
* in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
29572
|
+
* water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
|
|
29573
|
+
* block as well, like water, whose surface then shows from under it */
|
|
29574
|
+
setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
|
|
29530
29575
|
{
|
|
29531
29576
|
false&&ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
|
|
29532
29577
|
const f = /** @type {any} */ (faces);
|
|
29533
29578
|
const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
|
|
29534
29579
|
[f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
|
|
29535
29580
|
false&&ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
|
|
29536
|
-
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
|
|
29581
|
+
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
|
|
29537
29582
|
this.rebuild();
|
|
29538
29583
|
}
|
|
29539
29584
|
|
|
29540
29585
|
/** A block type's faces and how it is seen through
|
|
29541
29586
|
* @param {number} type
|
|
29542
|
-
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
|
|
29587
|
+
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
|
|
29543
29588
|
* @ignore */
|
|
29544
29589
|
blockType(type)
|
|
29545
29590
|
{
|
|
29546
|
-
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false
|
|
29591
|
+
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
|
|
29592
|
+
doubleSided: false};
|
|
29547
29593
|
}
|
|
29548
29594
|
|
|
29549
29595
|
/** Build every chunk again, after changing data directly or ambientOcclusion */
|
|
@@ -29613,6 +29659,11 @@ class VoxelMap extends EngineObject3D
|
|
|
29613
29659
|
colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
|
|
29614
29660
|
}
|
|
29615
29661
|
mesh.addStrip(points, normal, uvs, colors);
|
|
29662
|
+
|
|
29663
|
+
// a double sided block shows the face from inside too, its corners the other way round
|
|
29664
|
+
if (block.doubleSided)
|
|
29665
|
+
mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
|
|
29666
|
+
[uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
|
|
29616
29667
|
}
|
|
29617
29668
|
}
|
|
29618
29669
|
this.chunkMeshes[index]?.dispose();
|
package/package.json
CHANGED
package/plugins/box2d.js
CHANGED
|
@@ -414,7 +414,7 @@ class Box2dObject extends EngineObject
|
|
|
414
414
|
|
|
415
415
|
const points = [];
|
|
416
416
|
const radius = diameter/2;
|
|
417
|
-
for (let i=sides; i
|
|
417
|
+
for (let i=sides; i-- > 0;) // a count that is not a whole number still ends
|
|
418
418
|
points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
|
|
419
419
|
return this.addPoly(points, density, friction, restitution, isSensor);
|
|
420
420
|
}
|
package/plugins/render3d.js
CHANGED
|
@@ -4776,10 +4776,11 @@ class Light3D extends EngineObject3D
|
|
|
4776
4776
|
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
4777
4777
|
* @param {number} [intensity] - How bright it is
|
|
4778
4778
|
* @param {number} [saturation] - How colorful the ghosts are
|
|
4779
|
+
* @param {Color} [color] - Tints the flare, with the light's own color
|
|
4779
4780
|
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
4780
|
-
addFlare(size, count, intensity, saturation)
|
|
4781
|
+
addFlare(size, count, intensity, saturation, color)
|
|
4781
4782
|
{
|
|
4782
|
-
const flare = new LensFlare3D(size, count, intensity, saturation);
|
|
4783
|
+
const flare = new LensFlare3D(size, count, intensity, saturation, color);
|
|
4783
4784
|
this.flare = flare;
|
|
4784
4785
|
return flare;
|
|
4785
4786
|
}
|
|
@@ -549,9 +549,11 @@ function editor3DLevelLoaded(level)
|
|
|
549
549
|
level.objects = editor3DCopy(saved.objects);
|
|
550
550
|
for (const name of editor3DLevelPartNames)
|
|
551
551
|
parts[name] ? level[name] = editor3DCopy(parts[name]) : delete level[name];
|
|
552
|
+
saved.stale && console.warn(`LittleJS editor: the edits brought back to ${record.fileName} are older than ` +
|
|
553
|
+
'the last ones, which storage had no room for');
|
|
552
554
|
}
|
|
553
|
-
else if (saved)
|
|
554
|
-
record.pending = saved;
|
|
555
|
+
else if (saved && isArray(saved.objects))
|
|
556
|
+
record.pending = saved; // one that is broken is passed by, and the next autosave writes over it
|
|
555
557
|
}
|
|
556
558
|
|
|
557
559
|
// called by level3DLoad for each object, with what its type made
|
|
@@ -1575,7 +1575,7 @@ function render3DFlareTile(shape)
|
|
|
1575
1575
|
* with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
|
|
1576
1576
|
* give it elements of your own, which may be tiles of the game's
|
|
1577
1577
|
* - visible is how much of the sun shows, 0 to 1, eased over fadeTime, there for a game to read; it is 0 while the
|
|
1578
|
-
* sun is off the screen, where nothing is tested
|
|
1578
|
+
* sun is too far off the screen for the flare to show, a third of the screen past its edge, where nothing is tested
|
|
1579
1579
|
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
1580
1580
|
* through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
|
|
1581
1581
|
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
@@ -1622,7 +1622,8 @@ class LensFlare3D extends EngineObject3D
|
|
|
1622
1622
|
this.elements = undefined;
|
|
1623
1623
|
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
1624
1624
|
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
1625
|
-
* in front of the light,
|
|
1625
|
+
* in front of the light but its lamp, a mesh around it no wider than half its radius, and a spotlight's
|
|
1626
|
+
* shows from inside its beam only; a DirectionalLight3D's is far
|
|
1626
1627
|
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
1627
1628
|
* sets this
|
|
1628
1629
|
* @type {Light3D|undefined} */
|
|
@@ -1762,13 +1763,15 @@ class LensFlare3D extends EngineObject3D
|
|
|
1762
1763
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
1763
1764
|
return true;
|
|
1764
1765
|
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
1765
|
-
// is in
|
|
1766
|
+
// is in, and no wider than half the light's reach, since a room or a whole level has a box around its lights
|
|
1767
|
+
// too, and its walls do hide them from outside
|
|
1766
1768
|
const lamp = (o)=>
|
|
1767
1769
|
{
|
|
1768
1770
|
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
1769
|
-
const matrix = render3DObjectMatrix(o);
|
|
1770
|
-
|
|
1771
|
-
|
|
1771
|
+
const matrix = render3DObjectMatrix(o), b = o.mesh.bounds || o.mesh.getBounds();
|
|
1772
|
+
const side = max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z) * render3DMaxStretch(matrix.m);
|
|
1773
|
+
if (!matrix.determinant() || side > this.light.radius / 2) return false;
|
|
1774
|
+
const p = matrix.copy().invert().transformPoint(source.pos);
|
|
1772
1775
|
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
1773
1776
|
p.z >= b.min.z && p.z <= b.max.z;
|
|
1774
1777
|
};
|
|
@@ -89,7 +89,7 @@ class VoxelMap extends EngineObject3D
|
|
|
89
89
|
/** @type {Array<Vector3>} */
|
|
90
90
|
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
91
91
|
this.chunksChanged = new Set;
|
|
92
|
-
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
|
|
92
|
+
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
|
|
93
93
|
this.blockTypes = [];
|
|
94
94
|
this.tiles = new Map; // tile index to the uvs of its corners
|
|
95
95
|
|
|
@@ -161,27 +161,29 @@ class VoxelMap extends EngineObject3D
|
|
|
161
161
|
* @param {number} type - 1 to 255
|
|
162
162
|
* @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
|
|
163
163
|
* six in the order +x, -x, +y, -y, +z, -z, or the side's with the top and bottom's, which default to the side's
|
|
164
|
-
* @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes
|
|
165
|
-
* leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
166
|
-
* transparent stage, and see-through too
|
|
167
|
-
|
|
164
|
+
* @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
|
|
165
|
+
* in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
166
|
+
* water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
|
|
167
|
+
* block as well, like water, whose surface then shows from under it */
|
|
168
|
+
setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
|
|
168
169
|
{
|
|
169
170
|
ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
|
|
170
171
|
const f = /** @type {any} */ (faces);
|
|
171
172
|
const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
|
|
172
173
|
[f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
|
|
173
174
|
ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
|
|
174
|
-
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
|
|
175
|
+
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
|
|
175
176
|
this.rebuild();
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
/** A block type's faces and how it is seen through
|
|
179
180
|
* @param {number} type
|
|
180
|
-
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
|
|
181
|
+
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
|
|
181
182
|
* @ignore */
|
|
182
183
|
blockType(type)
|
|
183
184
|
{
|
|
184
|
-
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false
|
|
185
|
+
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
|
|
186
|
+
doubleSided: false};
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
/** Build every chunk again, after changing data directly or ambientOcclusion */
|
|
@@ -251,6 +253,11 @@ class VoxelMap extends EngineObject3D
|
|
|
251
253
|
colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
|
|
252
254
|
}
|
|
253
255
|
mesh.addStrip(points, normal, uvs, colors);
|
|
256
|
+
|
|
257
|
+
// a double sided block shows the face from inside too, its corners the other way round
|
|
258
|
+
if (block.doubleSided)
|
|
259
|
+
mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
|
|
260
|
+
[uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
|
|
254
261
|
}
|
|
255
262
|
}
|
|
256
263
|
this.chunkMeshes[index]?.dispose();
|
package/src/engine.js
CHANGED
package/src/engineEditor.js
CHANGED
|
@@ -570,8 +570,8 @@ function editorMapRestore(map)
|
|
|
570
570
|
if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
|
|
571
571
|
editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
|
|
572
572
|
editorDiscardPending(record);
|
|
573
|
-
else if (!sameFile || !
|
|
574
|
-
!editorCopyData(data, saved.layers))
|
|
573
|
+
else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
|
|
574
|
+
!editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
|
|
575
575
|
{
|
|
576
576
|
record.pending = saved;
|
|
577
577
|
console.warn(`LittleJS editor: ${record.fileName} changed since its autosaved edits, ` +
|
|
@@ -870,6 +870,11 @@ function editorCopyData(data, saved)
|
|
|
870
870
|
return true;
|
|
871
871
|
}
|
|
872
872
|
|
|
873
|
+
// are an autosave's tiles a layer for each of the map's, each of its own size, so the map is resized only for tiles
|
|
874
|
+
// that fit it
|
|
875
|
+
const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) && saved.layers.length === count &&
|
|
876
|
+
saved.layers.every((layer)=> isArray(layer) && layer.length === width * height);
|
|
877
|
+
|
|
873
878
|
// remember a map's tile data and objects, or forget them when they are back to the file
|
|
874
879
|
function editorAutosave(record)
|
|
875
880
|
{
|
|
@@ -898,7 +903,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
898
903
|
{
|
|
899
904
|
if (!editorHas('onRestart')) return;
|
|
900
905
|
const before = editorMapSnapshot(record);
|
|
901
|
-
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
906
|
+
editorSetMapSnapshot(record, {width, height, layers: data, objects}, keepUnknown);
|
|
902
907
|
editorMapChanged(record, before);
|
|
903
908
|
return;
|
|
904
909
|
}
|
|
@@ -1015,13 +1020,15 @@ function editorMapSnapshot(record)
|
|
|
1015
1020
|
return editorObjectsCopy({width: map.width, height: map.height, layers: editorTileLayerData(map.layers), objects});
|
|
1016
1021
|
}
|
|
1017
1022
|
|
|
1018
|
-
// give a map a size, tiles and objects, the game's Restart hook makes its layers again
|
|
1019
|
-
|
|
1023
|
+
// give a map a size, tiles and objects, the game's Restart hook makes its layers again; keepUnknown leaves an object
|
|
1024
|
+
// layer past the snapshot's as it is, as applying an autosave from before the file had it does
|
|
1025
|
+
function editorSetMapSnapshot(record, snapshot, keepUnknown=false)
|
|
1020
1026
|
{
|
|
1021
1027
|
const map = record.map;
|
|
1022
1028
|
editorResizeMap(map, snapshot.width, snapshot.height);
|
|
1023
1029
|
editorCopyData(editorTileLayerData(map.layers), snapshot.layers);
|
|
1024
1030
|
editorRestoreObjects(map, snapshot);
|
|
1031
|
+
if (!keepUnknown)
|
|
1025
1032
|
for (const group of editorObjectGroups(map.layers).slice(snapshot.objects.length))
|
|
1026
1033
|
group.objects = []; // an Objects layer the editor made since
|
|
1027
1034
|
editorClearSelections();
|
|
@@ -2181,9 +2188,13 @@ function editorPaletteTiles(layer)
|
|
|
2181
2188
|
if (!image) return [];
|
|
2182
2189
|
if (layer.palette?.image === image) return layer.palette.tiles;
|
|
2183
2190
|
|
|
2184
|
-
|
|
2191
|
+
// a sheet read by its columns, a tileset with a margin or a spacing, is read with frame(), which asserts the
|
|
2192
|
+
// frame is inside the image, so a row past the image's end is found before its frame is made
|
|
2193
|
+
const tiles = [], base = live.tileInfo, columns = base.columns;
|
|
2185
2194
|
for (let i = 0; i < 4096; ++i)
|
|
2186
2195
|
{
|
|
2196
|
+
if (columns && base.pos.y + (i / columns | 0) * (base.size.y + base.padding*2) + base.size.y > image.height)
|
|
2197
|
+
break;
|
|
2187
2198
|
const t = editorTileInfo(live, i);
|
|
2188
2199
|
if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
|
|
2189
2200
|
tiles.push({tile: i, tileInfo: t});
|
package/src/engineSettings.js
CHANGED
|
@@ -625,7 +625,11 @@ function setGLEnable(enable)
|
|
|
625
625
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
626
626
|
* @param {number} sides
|
|
627
627
|
* @memberof Settings */
|
|
628
|
-
function setGLCircleSides(sides)
|
|
628
|
+
function setGLCircleSides(sides)
|
|
629
|
+
{
|
|
630
|
+
ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
|
|
631
|
+
glCircleSides = sides;
|
|
632
|
+
}
|
|
629
633
|
|
|
630
634
|
/** Set default size of tiles in pixels
|
|
631
635
|
* @param {Vector2} size
|