littlejsengine 1.23.1 → 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 +261 -0
- package/COPYRIGHT.txt +0 -27
- package/FAQ.md +4 -9
- package/README.md +4 -3
- package/REFERENCE.md +34 -18
- package/dist/littlejs.d.ts +160 -105
- package/dist/littlejs.esm.js +490 -277
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +489 -273
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +463 -264
- package/package.json +2 -1
- package/plugins/box2d.js +1 -1
- package/plugins/pluginExport.js +0 -4
- package/plugins/render3d.js +93 -17
- package/plugins/render3dEditor.js +9 -3
- package/plugins/render3dExtras.js +169 -70
- package/plugins/render3dLevel.js +24 -4
- package/plugins/render3dVoxels.js +15 -8
- package/src/engine.js +1 -1
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +0 -1
- package/src/engineEditor.js +17 -6
- package/src/engineExport.js +1 -0
- package/src/engineSettings.js +5 -1
- package/src/engineTileLayer.js +154 -1
- package/plugins/zzfxm.js +0 -162
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.24.1';
|
|
39
39
|
|
|
40
40
|
/** Frames per second to update
|
|
41
41
|
* @type {number}
|
|
@@ -4084,7 +4084,11 @@ function setGLEnable(enable)
|
|
|
4084
4084
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
4085
4085
|
* @param {number} sides
|
|
4086
4086
|
* @memberof Settings */
|
|
4087
|
-
function setGLCircleSides(sides)
|
|
4087
|
+
function setGLCircleSides(sides)
|
|
4088
|
+
{
|
|
4089
|
+
ASSERT(isNumber(sides) && sides >= 3, 'circles need at least 3 sides');
|
|
4090
|
+
glCircleSides = sides;
|
|
4091
|
+
}
|
|
4088
4092
|
|
|
4089
4093
|
/** Set default size of tiles in pixels
|
|
4090
4094
|
* @param {Vector2} size
|
|
@@ -8430,7 +8434,7 @@ function touchGamepadPointerUp(e)
|
|
|
8430
8434
|
* - 2D spatial audio based on camera position with distance-based falloff
|
|
8431
8435
|
* - Sound instance management (pause, resume, stop)
|
|
8432
8436
|
* - Speech synthesis for text-to-speech
|
|
8433
|
-
* - Music playback
|
|
8437
|
+
* - Music playback from audio files, looping by default
|
|
8434
8438
|
* - Web Audio API integration with master gain control
|
|
8435
8439
|
* - Sounds and the master bus can route through effects, see the audio effects plugin
|
|
8436
8440
|
* @namespace Audio
|
|
@@ -9308,7 +9312,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
9308
9312
|
}
|
|
9309
9313
|
|
|
9310
9314
|
///////////////////////////////////////////////////////////////////////////////
|
|
9311
|
-
//
|
|
9315
|
+
// ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
9312
9316
|
|
|
9313
9317
|
/** Generate and play a ZzFX sound
|
|
9314
9318
|
*
|
|
@@ -9593,6 +9597,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
9593
9597
|
* Load tile layers from exported data
|
|
9594
9598
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
9595
9599
|
* tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
|
|
9600
|
+
* - A tileset kept in the map with a margin or a spacing, a sheet with gaps between its tiles, is read where its
|
|
9601
|
+
* tiles are, whatever padding the tile info has; one in a file of its own (a tsx) is not read, pass a tile info
|
|
9602
|
+
* with its padding
|
|
9603
|
+
* - An LDtk level loads through tileLayersFromLDtk, which makes it a map like these
|
|
9596
9604
|
* - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
|
|
9597
9605
|
* (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
|
|
9598
9606
|
* visibility carry to the layers inside it
|
|
@@ -9602,7 +9610,7 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
9602
9610
|
* @param {Object} tileMapData - Level data from exported data
|
|
9603
9611
|
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
|
|
9604
9612
|
* @param {number} [renderOrder] - Render order of the top layer
|
|
9605
|
-
* @param {number}
|
|
9613
|
+
* @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
|
|
9606
9614
|
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
9607
9615
|
* @return {Array<TileCollisionLayer>}
|
|
9608
9616
|
* @memberof TileLayers */
|
|
@@ -9625,6 +9633,19 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
9625
9633
|
ASSERT(tileMapData.width && tileMapData.height);
|
|
9626
9634
|
ASSERT(tileMapData.layers && tileMapData.layers.length);
|
|
9627
9635
|
|
|
9636
|
+
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
9637
|
+
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
9638
|
+
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
9639
|
+
const tileset = tileMapData.tilesets?.[0];
|
|
9640
|
+
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
9641
|
+
{
|
|
9642
|
+
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
9643
|
+
const width = tileInfo.textureInfo?.size.x || 0;
|
|
9644
|
+
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
9645
|
+
const origin = tileInfo.pos.subtract(vec2(tileInfo.padding)); // where the sheet starts, in an atlas too
|
|
9646
|
+
tileInfo = new TileInfo(origin.add(vec2(margin)), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
9647
|
+
}
|
|
9648
|
+
|
|
9628
9649
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
9629
9650
|
/** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
|
|
9630
9651
|
const layers = [];
|
|
@@ -9653,6 +9674,14 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
9653
9674
|
};
|
|
9654
9675
|
addLayers(tileMapData.layers, WHITE, true);
|
|
9655
9676
|
|
|
9677
|
+
// a layer named for collision is the first tile layer of that name, which stays the same when layers are added
|
|
9678
|
+
if (typeof collisionLayer === 'string')
|
|
9679
|
+
{
|
|
9680
|
+
const name = collisionLayer;
|
|
9681
|
+
collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
|
|
9682
|
+
ASSERT(collisionLayer >= 0, 'no tile layer named ' + name);
|
|
9683
|
+
}
|
|
9684
|
+
|
|
9656
9685
|
// create tile layers and fill with data
|
|
9657
9686
|
const tileLayers = [];
|
|
9658
9687
|
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
@@ -9702,6 +9731,134 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
9702
9731
|
function tileLayersColor(hex)
|
|
9703
9732
|
{ return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
|
|
9704
9733
|
|
|
9734
|
+
// the Tiled property type of an LDtk field type, for the ones Tiled has a property for
|
|
9735
|
+
const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
|
|
9736
|
+
Color: 'color', FilePath: 'file'};
|
|
9737
|
+
|
|
9738
|
+
/**
|
|
9739
|
+
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
9740
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled; where LDtk stacks
|
|
9741
|
+
* tiles in a cell, an edge over a fill, each tile over another goes in a layer of its own just above, named with
|
|
9742
|
+
* (2), (3) and so on, and a tile LDtk draws see-through goes in one of its own with that opacity, times the
|
|
9743
|
+
* layer's, named with it, like Ground .25
|
|
9744
|
+
* - An IntGrid layer is a hidden layer of its values under its own name, for collision, whatever tiles its rules
|
|
9745
|
+
* make, which are layers over it named with tiles, like Collisions tiles; pass the name as collisionLayer, since
|
|
9746
|
+
* the stacked and see-through layers change the indices from level to level:
|
|
9747
|
+
* tileLayersLoad(map, tile(0, 16), 0, 'Collisions')
|
|
9748
|
+
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
9749
|
+
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
9750
|
+
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
9751
|
+
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
9752
|
+
* another grid size or another tileset is left out, with a warning in debug builds, and a project with no tileset
|
|
9753
|
+
* keeps its tiles for the tile info the game gives; a project of several worlds has its levels counted in order
|
|
9754
|
+
* - Layer offsets are not read
|
|
9755
|
+
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
9756
|
+
* @param {Object} ldtk - The LDtk project, its JSON
|
|
9757
|
+
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
9758
|
+
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
9759
|
+
* @example
|
|
9760
|
+
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
9761
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 'Collisions'); // its IntGrid layer is solid
|
|
9762
|
+
* objectLayersLoad(map);
|
|
9763
|
+
* @memberof TileLayers */
|
|
9764
|
+
function tileLayersFromLDtk(ldtk, level=0)
|
|
9765
|
+
{
|
|
9766
|
+
const levels = ldtk?.levels?.length ? ldtk.levels : (ldtk?.worlds || []).flatMap((w)=> w.levels || []);
|
|
9767
|
+
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
9768
|
+
if (!data)
|
|
9769
|
+
throw new Error('tileLayersFromLDtk: no level ' + level);
|
|
9770
|
+
if (!isArray(data.layerInstances))
|
|
9771
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' has no layers, levels saved as separate files are not read');
|
|
9772
|
+
const instances = data.layerInstances;
|
|
9773
|
+
const tiled = (l)=> l.__type !== 'Entities';
|
|
9774
|
+
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
9775
|
+
const grid = first?.__gridSize || 16;
|
|
9776
|
+
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
9777
|
+
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
9778
|
+
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
9779
|
+
|
|
9780
|
+
// the tileset of the first layer that has one, where its tiles are in its image
|
|
9781
|
+
const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
|
|
9782
|
+
if (tileset)
|
|
9783
|
+
map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
|
|
9784
|
+
image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
|
|
9785
|
+
tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
|
|
9786
|
+
spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
|
|
9787
|
+
|
|
9788
|
+
// LDtk lists the top layer first, Tiled the bottom one
|
|
9789
|
+
for (let i = instances.length; i--;)
|
|
9790
|
+
{
|
|
9791
|
+
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
9792
|
+
if (!tiled(instance))
|
|
9793
|
+
{
|
|
9794
|
+
const objects = (instance.entityInstances || []).map((entity)=>
|
|
9795
|
+
{
|
|
9796
|
+
// an entity is placed by its pivot, an object here by its middle
|
|
9797
|
+
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
9798
|
+
const properties = [];
|
|
9799
|
+
for (const field of entity.fieldInstances || [])
|
|
9800
|
+
{
|
|
9801
|
+
const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
|
|
9802
|
+
type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
|
|
9803
|
+
}
|
|
9804
|
+
return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
|
|
9805
|
+
y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
|
|
9806
|
+
});
|
|
9807
|
+
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
9808
|
+
continue;
|
|
9809
|
+
}
|
|
9810
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
|
|
9811
|
+
const ownTiles = !tiles.length || !tileset ||
|
|
9812
|
+
instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
|
|
9813
|
+
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
|
|
9814
|
+
{
|
|
9815
|
+
debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
|
|
9816
|
+
continue;
|
|
9817
|
+
}
|
|
9818
|
+
// an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
|
|
9819
|
+
const empty = ()=> new Array(width * height).fill(0), opacity = instance.__opacity ?? 1;
|
|
9820
|
+
const values = instance.__type === 'IntGrid';
|
|
9821
|
+
if (values)
|
|
9822
|
+
{
|
|
9823
|
+
const data = empty();
|
|
9824
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
|
|
9825
|
+
map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
|
|
9826
|
+
if (!tiles.length) continue;
|
|
9827
|
+
}
|
|
9828
|
+
|
|
9829
|
+
// a layer for each depth in a cell and each opacity, the first tiles drawn solid first
|
|
9830
|
+
const tilesName = values ? name + ' tiles' : name;
|
|
9831
|
+
const stacks = [{depth: 0, alpha: 1, data: empty()}], stack = (depth, alpha)=>
|
|
9832
|
+
{
|
|
9833
|
+
let s = stacks.find((s)=> s.depth === depth && s.alpha === alpha);
|
|
9834
|
+
s || stacks.push(s = {depth, alpha, data: empty()});
|
|
9835
|
+
return s.data;
|
|
9836
|
+
};
|
|
9837
|
+
{
|
|
9838
|
+
// a tile's place in pixels, its tile in the sheet, its flips, bit 0 across and bit 1 down, which are
|
|
9839
|
+
// Tiled's two top bits, and its opacity; LDtk lists them in the order it draws them, so the first in a
|
|
9840
|
+
// cell is the bottom one and each after it goes a layer higher
|
|
9841
|
+
const depth = new Uint16Array(width * height);
|
|
9842
|
+
for (const t of tiles)
|
|
9843
|
+
{
|
|
9844
|
+
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
9845
|
+
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
9846
|
+
stack(depth[cell]++, round(clamp(t.a ?? 1) * 1e3) / 1e3)[cell] =
|
|
9847
|
+
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
9848
|
+
}
|
|
9849
|
+
}
|
|
9850
|
+
const visible = instance.visible !== false;
|
|
9851
|
+
stacks.sort((a, b)=> a.depth - b.depth || b.alpha - a.alpha);
|
|
9852
|
+
for (const {depth, alpha, data} of stacks)
|
|
9853
|
+
{
|
|
9854
|
+
const layerName = tilesName + (depth ? ` (${depth + 1})` : '') + (alpha < 1 ? ' ' + String(alpha).replace(/^0/, '') : '');
|
|
9855
|
+
map.layers.push({id: values || depth || alpha < 1 ? map.nextlayerid++ : id, name: layerName, type: 'tilelayer',
|
|
9856
|
+
width, height, data, opacity: opacity * alpha, visible, x: 0, y: 0});
|
|
9857
|
+
}
|
|
9858
|
+
}
|
|
9859
|
+
return map;
|
|
9860
|
+
}
|
|
9861
|
+
|
|
9705
9862
|
///////////////////////////////////////////////////////////////////////////////
|
|
9706
9863
|
// Object layers
|
|
9707
9864
|
|
|
@@ -14443,166 +14600,6 @@ class Light extends EngineObject
|
|
|
14443
14600
|
}
|
|
14444
14601
|
}
|
|
14445
14602
|
|
|
14446
|
-
/**
|
|
14447
|
-
* LittleJS ZzFXM Plugin
|
|
14448
|
-
* - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
|
|
14449
|
-
* https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
|
|
14450
|
-
* @namespace ZzFXM
|
|
14451
|
-
*/
|
|
14452
|
-
|
|
14453
|
-
/**
|
|
14454
|
-
* Music Object - Stores a zzfx music track for later use
|
|
14455
|
-
*
|
|
14456
|
-
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
14457
|
-
* @extends Sound
|
|
14458
|
-
* @memberof ZzFXM
|
|
14459
|
-
* @example
|
|
14460
|
-
* // create some music
|
|
14461
|
-
* const music_example = new ZzFXMusic(
|
|
14462
|
-
* [
|
|
14463
|
-
* [ // instruments
|
|
14464
|
-
* [,0,400] // simple note
|
|
14465
|
-
* ],
|
|
14466
|
-
* [ // patterns
|
|
14467
|
-
* [ // pattern 1
|
|
14468
|
-
* [ // channel 0
|
|
14469
|
-
* 0, -1, // instrument 0, left speaker
|
|
14470
|
-
* 1, 0, 9, 1 // channel notes
|
|
14471
|
-
* ],
|
|
14472
|
-
* [ // channel 1
|
|
14473
|
-
* 0, 1, // instrument 0, right speaker
|
|
14474
|
-
* 0, 12, 17, -1 // channel notes
|
|
14475
|
-
* ]
|
|
14476
|
-
* ],
|
|
14477
|
-
* ],
|
|
14478
|
-
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
14479
|
-
* 90 // BPM
|
|
14480
|
-
* ]);
|
|
14481
|
-
*
|
|
14482
|
-
* // play the music on a loop
|
|
14483
|
-
* music_example.playMusic();
|
|
14484
|
-
*/
|
|
14485
|
-
class ZzFXMusic extends Sound
|
|
14486
|
-
{
|
|
14487
|
-
/** Create a music object and cache the zzfx music samples for later use
|
|
14488
|
-
* @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
|
|
14489
|
-
*/
|
|
14490
|
-
constructor(zzfxMusic)
|
|
14491
|
-
{
|
|
14492
|
-
super(undefined);
|
|
14493
|
-
|
|
14494
|
-
if (!soundEnable || headlessMode) return;
|
|
14495
|
-
const [instruments, patterns, sequence, BPM] = zzfxMusic;
|
|
14496
|
-
super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
|
|
14497
|
-
this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
|
|
14498
|
-
this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
|
|
14499
|
-
}
|
|
14500
|
-
}
|
|
14501
|
-
|
|
14502
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
14503
|
-
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
14504
|
-
|
|
14505
|
-
/** Generate samples for a ZzFM song with given parameters
|
|
14506
|
-
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
14507
|
-
* @param {Array} patterns - Array of pattern data
|
|
14508
|
-
* @param {Array} sequence - Array of pattern indexes
|
|
14509
|
-
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
14510
|
-
* @return {Array} - Left and right channel sample data
|
|
14511
|
-
* @memberof ZzFXM */
|
|
14512
|
-
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
14513
|
-
{
|
|
14514
|
-
let i, j, k;
|
|
14515
|
-
let instrumentParameters;
|
|
14516
|
-
let note;
|
|
14517
|
-
let sample;
|
|
14518
|
-
let patternChannel;
|
|
14519
|
-
let notFirstBeat;
|
|
14520
|
-
let stop;
|
|
14521
|
-
let instrument;
|
|
14522
|
-
let attenuation;
|
|
14523
|
-
let outSampleOffset;
|
|
14524
|
-
let isSequenceEnd;
|
|
14525
|
-
let sampleOffset = 0;
|
|
14526
|
-
let nextSampleOffset;
|
|
14527
|
-
let sampleBuffer = [];
|
|
14528
|
-
let leftChannelBuffer = [];
|
|
14529
|
-
let rightChannelBuffer = [];
|
|
14530
|
-
let channelIndex = 0;
|
|
14531
|
-
let panning = 0;
|
|
14532
|
-
let hasMore = 1;
|
|
14533
|
-
let sampleCache = {};
|
|
14534
|
-
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
14535
|
-
|
|
14536
|
-
// for each channel in order until there are no more
|
|
14537
|
-
for (; hasMore; channelIndex++) {
|
|
14538
|
-
|
|
14539
|
-
// reset current values
|
|
14540
|
-
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
14541
|
-
|
|
14542
|
-
// for each pattern in sequence
|
|
14543
|
-
sequence.forEach((patternIndex, sequenceIndex)=> {
|
|
14544
|
-
// get pattern for current channel, use empty 1 note pattern if none found
|
|
14545
|
-
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
14546
|
-
|
|
14547
|
-
// check if there are more channels
|
|
14548
|
-
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
14549
|
-
|
|
14550
|
-
// get next offset, use the length of first channel
|
|
14551
|
-
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
14552
|
-
// for each beat in pattern, plus one extra if end of sequence
|
|
14553
|
-
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
14554
|
-
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
14555
|
-
|
|
14556
|
-
// <channel-note>
|
|
14557
|
-
note = patternChannel[i];
|
|
14558
|
-
|
|
14559
|
-
// stop if end, different instrument or new note
|
|
14560
|
-
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
14561
|
-
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
14562
|
-
|
|
14563
|
-
// fill buffer with samples for previous beat, most cpu intensive part
|
|
14564
|
-
for (j = 0; j < beatLength && notFirstBeat;
|
|
14565
|
-
|
|
14566
|
-
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
14567
|
-
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
14568
|
-
) {
|
|
14569
|
-
// copy sample to stereo buffers with panning
|
|
14570
|
-
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
14571
|
-
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
14572
|
-
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
14573
|
-
}
|
|
14574
|
-
|
|
14575
|
-
// set up for next note
|
|
14576
|
-
if (note) {
|
|
14577
|
-
// set attenuation
|
|
14578
|
-
attenuation = note % 1;
|
|
14579
|
-
panning = patternChannel[1] || 0;
|
|
14580
|
-
if (note |= 0) {
|
|
14581
|
-
// get cached sample
|
|
14582
|
-
sampleBuffer = sampleCache[
|
|
14583
|
-
[
|
|
14584
|
-
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
14585
|
-
note
|
|
14586
|
-
]
|
|
14587
|
-
] = sampleCache[[instrument, note]] || (
|
|
14588
|
-
// add sample to cache
|
|
14589
|
-
instrumentParameters = [...instruments[instrument]],
|
|
14590
|
-
instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
|
|
14591
|
-
|
|
14592
|
-
// allow negative values to stop notes
|
|
14593
|
-
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
14594
|
-
);
|
|
14595
|
-
}
|
|
14596
|
-
}
|
|
14597
|
-
}
|
|
14598
|
-
|
|
14599
|
-
// update the sample offset
|
|
14600
|
-
outSampleOffset = nextSampleOffset;
|
|
14601
|
-
});
|
|
14602
|
-
}
|
|
14603
|
-
|
|
14604
|
-
return [leftChannelBuffer, rightChannelBuffer];
|
|
14605
|
-
}
|
|
14606
14603
|
/**
|
|
14607
14604
|
* LittleJS Audio Effects Plugin
|
|
14608
14605
|
* - Web Audio effects with a wet/dry mix: filter, reverb, delay, distortion, compressor
|
|
@@ -17418,7 +17415,7 @@ class Box2dObject extends EngineObject
|
|
|
17418
17415
|
|
|
17419
17416
|
const points = [];
|
|
17420
17417
|
const radius = diameter/2;
|
|
17421
|
-
for (let i=sides; i
|
|
17418
|
+
for (let i=sides; i-- > 0;) // a count that is not a whole number still ends
|
|
17422
17419
|
points.push(vec2(radius,0).rotate((i+.5)/sides*PI*2));
|
|
17423
17420
|
return this.addPoly(points, density, friction, restitution, isSensor);
|
|
17424
17421
|
}
|
|
@@ -24025,8 +24022,8 @@ class Render3DPlugin
|
|
|
24025
24022
|
}
|
|
24026
24023
|
|
|
24027
24024
|
/** Find the nearest object under a screen position or along a ray, for clicking on things
|
|
24028
|
-
* -
|
|
24029
|
-
*
|
|
24025
|
+
* - A mesh is hit on its triangles, the ones that face the ray as they are drawn, both sides of a doubleSided
|
|
24026
|
+
* mesh; a sprite is hit as the quad it draws, and a height map or a voxel map on its surface
|
|
24030
24027
|
* - engineObjectsRaycast3D is the other half of this, every object along a ray instead of the nearest
|
|
24031
24028
|
* @param {Vector2|Ray3D} from - A screen position like mousePosScreen, or a ray to look along
|
|
24032
24029
|
* @param {Array<EngineObject>} [objects] - Defaults to every object; only those with a mesh or a sprite count
|
|
@@ -27393,8 +27390,8 @@ function engineObjectsCollect3D(pos, size, objects=engineObjects, testCenters=fa
|
|
|
27393
27390
|
return collected;
|
|
27394
27391
|
}
|
|
27395
27392
|
|
|
27396
|
-
// how far along a ray an object is hit, or undefined for a miss;
|
|
27397
|
-
//
|
|
27393
|
+
// how far along a ray an object is hit, or undefined for a miss; a mesh is hit on its triangles, in its own space,
|
|
27394
|
+
// and a sprite as the quad it draws
|
|
27398
27395
|
function render3DRaycastObject(ray, o)
|
|
27399
27396
|
{
|
|
27400
27397
|
if (o.destroyed || !(o instanceof EngineObject3D)) return;
|
|
@@ -27411,25 +27408,55 @@ function render3DRaycastObject(ray, o)
|
|
|
27411
27408
|
const distance = raycastSphere(ray, center, radius);
|
|
27412
27409
|
if (distance === undefined) return;
|
|
27413
27410
|
|
|
27414
|
-
// the sphere
|
|
27415
|
-
//
|
|
27411
|
+
// the sphere and then the box in the mesh's own space are quick rejects, and a ray that meets the box is tested
|
|
27412
|
+
// against the triangles; the direction is not made unit length, so the distance holds in the world;
|
|
27416
27413
|
// a mesh flattened to nothing on an axis has no inverse, it is hit as a disc like a sprite
|
|
27417
27414
|
if (!matrix.determinant()) return render3DRaycastDisc(ray, center, radius);
|
|
27418
27415
|
const inverse = matrix.copy().invert(), bounds = mesh.bounds || mesh.getBounds();
|
|
27419
27416
|
const local = new Ray3D(inverse.transformPoint(ray.origin), inverse.transformDirection(ray.direction));
|
|
27420
27417
|
const hit = raycastBox(local, bounds.min.add(bounds.max).scale(.5), bounds.max.subtract(bounds.min));
|
|
27421
|
-
if (hit
|
|
27422
|
-
|
|
27423
|
-
|
|
27424
|
-
|
|
27425
|
-
|
|
27426
|
-
|
|
27418
|
+
if (hit === undefined || !ray.direction.lengthSquared())
|
|
27419
|
+
return hit; // a ray of no length is where it starts, inside the box
|
|
27420
|
+
return render3DRaycastMesh(local, mesh);
|
|
27421
|
+
}
|
|
27422
|
+
|
|
27423
|
+
// how far along a ray, in the mesh's own space, its nearest triangle is hit, or undefined: the triangles that face
|
|
27424
|
+
// the ray as the pass draws them, clockwise from the front, and the back faces too of a doubleSided mesh; a strip's
|
|
27425
|
+
// triangle i is (i-2, i-1, i), the odd ones read the other way, and a join between its pieces has no area and is
|
|
27426
|
+
// never hit; nothing is kept between calls, so a mesh that changed is hit as it is now
|
|
27427
|
+
function render3DRaycastMesh(ray, mesh)
|
|
27428
|
+
{
|
|
27429
|
+
const points = mesh.points, indices = mesh.indices, both = mesh.doubleSided;
|
|
27430
|
+
const ox = ray.origin.x, oy = ray.origin.y, oz = ray.origin.z;
|
|
27431
|
+
const dx = ray.direction.x, dy = ray.direction.y, dz = ray.direction.z;
|
|
27432
|
+
const count = indices ? indices.length / 3 | 0 : points.length - 2;
|
|
27433
|
+
let nearest;
|
|
27434
|
+
for (let i = 0; i < count; ++i)
|
|
27427
27435
|
{
|
|
27428
|
-
|
|
27429
|
-
|
|
27430
|
-
|
|
27436
|
+
// the corners clockwise from the front: an indexed list is written the other way round
|
|
27437
|
+
let a, b, c;
|
|
27438
|
+
if (indices)
|
|
27439
|
+
a = points[indices[i*3]], b = points[indices[i*3+2]], c = points[indices[i*3+1]];
|
|
27440
|
+
else
|
|
27441
|
+
a = points[i & 1 ? i + 1 : i], b = points[i & 1 ? i : i + 1], c = points[i + 2];
|
|
27442
|
+
|
|
27443
|
+
// Moller and Trumbore: the determinant is the ray along the face's normal, below zero from the front
|
|
27444
|
+
const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
|
|
27445
|
+
const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
|
|
27446
|
+
const hx = dy * e2z - dz * e2y, hy = dz * e2x - dx * e2z, hz = dx * e2y - dy * e2x;
|
|
27447
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
27448
|
+
if (both ? !det : !(det < 0)) continue;
|
|
27449
|
+
const sx = ox - a.x, sy = oy - a.y, sz = oz - a.z;
|
|
27450
|
+
const u = (sx * hx + sy * hy + sz * hz) / det;
|
|
27451
|
+
if (!(u >= 0 && u <= 1)) continue;
|
|
27452
|
+
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
|
|
27453
|
+
const v = (dx * qx + dy * qy + dz * qz) / det;
|
|
27454
|
+
if (!(v >= 0 && u + v <= 1)) continue;
|
|
27455
|
+
const t = (e2x * qx + e2y * qy + e2z * qz) / det;
|
|
27456
|
+
if (t >= 0 && !(t >= nearest))
|
|
27457
|
+
nearest = t;
|
|
27431
27458
|
}
|
|
27432
|
-
return
|
|
27459
|
+
return nearest;
|
|
27433
27460
|
}
|
|
27434
27461
|
|
|
27435
27462
|
// a sprite is the quad drawBillboard draws, its size3D grown by the x and y scale as the draw does, facing the camera
|
|
@@ -27807,10 +27834,12 @@ class InstancedMesh3D extends EngineObject3D
|
|
|
27807
27834
|
* - radius is a world distance, so scale3D does not change it
|
|
27808
27835
|
* - An alpha, an intensity or a radius of 0 switches it off, and a light that is off takes none of those slots
|
|
27809
27836
|
* - Draws nothing but its glow, when it has one; add a small emissive mesh if the lamp itself should be seen
|
|
27837
|
+
* - addFlare gives it a lens flare, see LensFlare3D
|
|
27810
27838
|
* @extends EngineObject3D
|
|
27811
27839
|
* @memberof Render3D
|
|
27812
27840
|
* @example
|
|
27813
27841
|
* const torch = new Light3D(vec3(0, 3, 0), 10, hsl(.1, 1, .65));
|
|
27842
|
+
* torch.addFlare(); // light in the lens when the torch is in view
|
|
27814
27843
|
*/
|
|
27815
27844
|
class Light3D extends EngineObject3D
|
|
27816
27845
|
{
|
|
@@ -27845,6 +27874,50 @@ class Light3D extends EngineObject3D
|
|
|
27845
27874
|
* bright core */
|
|
27846
27875
|
this.glowFalloff = 1;
|
|
27847
27876
|
this.additive = true; // the glow is added on, in the transparent stage; a light with none draws nothing
|
|
27877
|
+
/** @type {LensFlare3D|undefined} */
|
|
27878
|
+
this.flareObject = undefined;
|
|
27879
|
+
}
|
|
27880
|
+
|
|
27881
|
+
/** Give the light a lens flare, made with the arguments of LensFlare3D, in place of the one it had
|
|
27882
|
+
* @param {number} [size] - Scales every part of it
|
|
27883
|
+
* @param {number} [count] - How many ghosts there are, besides the glow at the light
|
|
27884
|
+
* @param {number} [intensity] - How bright it is
|
|
27885
|
+
* @param {number} [saturation] - How colorful the ghosts are
|
|
27886
|
+
* @param {Color} [color] - Tints the flare, with the light's own color
|
|
27887
|
+
* @return {LensFlare3D} - The flare, to change: light.addFlare().shapes = ['hex'] */
|
|
27888
|
+
addFlare(size, count, intensity, saturation, color)
|
|
27889
|
+
{
|
|
27890
|
+
const flare = new LensFlare3D(size, count, intensity, saturation, color);
|
|
27891
|
+
this.flare = flare;
|
|
27892
|
+
return flare;
|
|
27893
|
+
}
|
|
27894
|
+
|
|
27895
|
+
/** The light's lens flare, undefined for none: addFlare makes it, or set a LensFlare3D of your own, and
|
|
27896
|
+
* destroying the flare takes it away
|
|
27897
|
+
* - The flare is the light's: it is attached to the light as its child, so it stays through a scene change
|
|
27898
|
+
* when the light does, and is destroyed with the light or when another takes its place
|
|
27899
|
+
* @type {LensFlare3D|undefined} */
|
|
27900
|
+
get flare() { return this.flareObject && !this.flareObject.destroyed ? this.flareObject : undefined; }
|
|
27901
|
+
|
|
27902
|
+
set flare(flare)
|
|
27903
|
+
{
|
|
27904
|
+
const old = this.flare; // a flare that was destroyed on its own is no flare
|
|
27905
|
+
if (flare === old) return;
|
|
27906
|
+
old && old.destroy();
|
|
27907
|
+
if (flare)
|
|
27908
|
+
{
|
|
27909
|
+
flare.light = this;
|
|
27910
|
+
flare.parent || this.addChild(flare);
|
|
27911
|
+
}
|
|
27912
|
+
this.flareObject = flare || undefined;
|
|
27913
|
+
}
|
|
27914
|
+
|
|
27915
|
+
/** Destroy the light, and its flare with it
|
|
27916
|
+
* @param {boolean} [immediate] */
|
|
27917
|
+
destroy(immediate)
|
|
27918
|
+
{
|
|
27919
|
+
this.flare = undefined;
|
|
27920
|
+
super.destroy(immediate);
|
|
27848
27921
|
}
|
|
27849
27922
|
|
|
27850
27923
|
/** Draw the glow, a quad facing the camera with a soft round glow on it, pulled toward the camera by half its
|
|
@@ -29363,31 +29436,74 @@ class Trail3D extends EngineObject3D
|
|
|
29363
29436
|
// lens flare
|
|
29364
29437
|
|
|
29365
29438
|
// the shapes a flare is made of, white on clear on one smooth texture made the first time it is asked for: a soft
|
|
29366
|
-
// glow, a flat disc with a brighter rim,
|
|
29439
|
+
// glow, a flat disc with a brighter rim, a ring, a hexagon like the blades of a lens, a streak across, and a star
|
|
29440
|
+
// of six rays; undefined headless or without a canvas
|
|
29441
|
+
const render3DFlareShapes = ['glow', 'disc', 'ring', 'hex', 'streak', 'star'];
|
|
29367
29442
|
let render3DFlareTiles;
|
|
29368
29443
|
function render3DFlareTile(shape)
|
|
29369
29444
|
{
|
|
29370
29445
|
if (!render3DFlareTiles)
|
|
29371
29446
|
{
|
|
29372
29447
|
if (headlessMode || !glContext || typeof OffscreenCanvas == 'undefined') return;
|
|
29373
|
-
const cell = 128, shapes =
|
|
29448
|
+
const cell = 128, shapes = render3DFlareShapes, context = createCanvasContext(cell * shapes.length, cell);
|
|
29374
29449
|
// each shape is how see-through it is from its middle out
|
|
29375
29450
|
const alpha =
|
|
29376
29451
|
{
|
|
29377
29452
|
glow: (t)=> engineGlowAlpha(t, 1),
|
|
29378
29453
|
disc: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
29379
29454
|
ring: (t)=> max(0, 1 - abs(t - .86) / .12),
|
|
29455
|
+
hex: (t)=> t < .8 ? .4 + .1 * t : t < .92 ? .48 + (t - .8) * 3 : (1 - t) / .08 * .84,
|
|
29456
|
+
streak: (t)=> (1 - t) ** 1.5,
|
|
29380
29457
|
};
|
|
29381
|
-
|
|
29458
|
+
// a round gradient from the middle of the cell out, squashed for a ray and cut to a path for the hexagon
|
|
29459
|
+
const fill = (shape, r, path)=>
|
|
29382
29460
|
{
|
|
29383
|
-
const
|
|
29384
|
-
const gradient = context.createRadialGradient(x, y, 0, x, y, r);
|
|
29461
|
+
const steps = 32, gradient = context.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
29385
29462
|
for (let k = 0; k <= steps; ++k)
|
|
29386
29463
|
gradient.addColorStop(k / steps, 'rgba(255,255,255,' + clamp(alpha[shape](k / steps)).toFixed(4) + ')');
|
|
29387
29464
|
context.fillStyle = gradient;
|
|
29388
29465
|
context.beginPath();
|
|
29389
|
-
context.arc(
|
|
29466
|
+
path ? path() : context.arc(0, 0, r, 0, 2 * PI);
|
|
29390
29467
|
context.fill();
|
|
29468
|
+
};
|
|
29469
|
+
const ray = (r, angle)=>
|
|
29470
|
+
{
|
|
29471
|
+
context.save();
|
|
29472
|
+
context.rotate(angle);
|
|
29473
|
+
context.scale(1, .07);
|
|
29474
|
+
fill('streak', r);
|
|
29475
|
+
context.restore();
|
|
29476
|
+
};
|
|
29477
|
+
shapes.forEach((shape, i)=>
|
|
29478
|
+
{
|
|
29479
|
+
const r = cell / 2 - 2;
|
|
29480
|
+
context.save();
|
|
29481
|
+
context.translate(i * cell + cell / 2, cell / 2);
|
|
29482
|
+
if (shape == 'hex')
|
|
29483
|
+
{
|
|
29484
|
+
// a gradient is round, so the hexagon is set pixel by pixel, by how far each is toward a side
|
|
29485
|
+
const image = context.createImageData(cell, cell), data = image.data, apothem = r * cos(PI / 6);
|
|
29486
|
+
for (let k = 0; k < cell * cell; ++k)
|
|
29487
|
+
{
|
|
29488
|
+
const x = abs(k % cell + .5 - cell / 2), y = abs((k / cell | 0) + .5 - cell / 2);
|
|
29489
|
+
const t = max(y, x * cos(PI / 6) + y / 2) / apothem;
|
|
29490
|
+
data[k * 4] = data[k * 4 + 1] = data[k * 4 + 2] = 255;
|
|
29491
|
+
data[k * 4 + 3] = t < 1 ? clamp(alpha.hex(t)) * 255 : 0;
|
|
29492
|
+
}
|
|
29493
|
+
context.putImageData(image, i * cell, 0);
|
|
29494
|
+
}
|
|
29495
|
+
else if (shape == 'streak')
|
|
29496
|
+
ray(r, 0);
|
|
29497
|
+
else if (shape == 'star')
|
|
29498
|
+
{
|
|
29499
|
+
for (let k = 3; k--;)
|
|
29500
|
+
ray(r, k * PI / 3 + PI / 2);
|
|
29501
|
+
context.scale(.3, .3);
|
|
29502
|
+
fill('glow', r);
|
|
29503
|
+
}
|
|
29504
|
+
else
|
|
29505
|
+
fill(shape, r);
|
|
29506
|
+
context.restore();
|
|
29391
29507
|
});
|
|
29392
29508
|
const texture = new TextureInfo(context.canvas, true, false, false); // smooth even in a pixel art game
|
|
29393
29509
|
render3DFlareTiles = new Map(shapes.map((shape, i)=>
|
|
@@ -29396,16 +29512,32 @@ function render3DFlareTile(shape)
|
|
|
29396
29512
|
return render3DFlareTiles.get(shape) || render3DFlareTiles.get('glow');
|
|
29397
29513
|
}
|
|
29398
29514
|
|
|
29515
|
+
/**
|
|
29516
|
+
* A part of a lens flare
|
|
29517
|
+
* @typedef {Object} LensFlareElement
|
|
29518
|
+
* @property {number} at - Where along the line: 0 the sun, 1 the middle of the screen, 2 as far past it
|
|
29519
|
+
* @property {number|Vector2} size - How big across, as a part of the screen's height, a vector for a part wider
|
|
29520
|
+
* than it is tall
|
|
29521
|
+
* @property {Color} color - Its color, the alpha how bright
|
|
29522
|
+
* @property {string} [shape] - glow, disc, ring, hex, streak or star, a glow when left out
|
|
29523
|
+
* @property {TileInfo} [tileInfo] - A tile of the game's own to draw in place of a shape, best white on clear
|
|
29524
|
+
* @property {number} [angle] - How far it is turned, in radians
|
|
29525
|
+
* @memberof Render3D
|
|
29526
|
+
*/
|
|
29527
|
+
|
|
29399
29528
|
/**
|
|
29400
29529
|
* LensFlare3D - The sun's lens flare, the old kind: a glow at the sun and a row of discs and rings of different
|
|
29401
29530
|
* sizes along the line from the sun through the middle of the screen
|
|
29402
29531
|
* - Make one and it shows, over the 3D scene and under what the game draws after, a HUD; destroy it to take it away
|
|
29403
29532
|
* - It follows render3D.sunDirection, and fades out as the sun leaves the screen or goes behind something
|
|
29533
|
+
* - A Light3D gets one of its own with light.addFlare()
|
|
29404
29534
|
* - flareSize, count, intensity and saturation set its look, seed picks another arrangement, and its color tints it,
|
|
29405
|
-
* with the sun's own color;
|
|
29406
|
-
*
|
|
29535
|
+
* with the sun's own color; shapes says what its ghosts are, glowSize and ghostSize how big its parts are; or
|
|
29536
|
+
* give it elements of your own, which may be tiles of the game's
|
|
29537
|
+
* - 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
|
|
29538
|
+
* sun is too far off the screen for the flare to show, a third of the screen past its edge, where nothing is tested
|
|
29407
29539
|
* - What hides the sun is found with a ray from the camera, against the level and every object that is not see
|
|
29408
|
-
* through, each
|
|
29540
|
+
* through, each on the triangles of its mesh, see render3D.pick; turn it off with occlusion
|
|
29409
29541
|
* - It needs WebGL, and it draws nothing in the shadow of renderAfter2D
|
|
29410
29542
|
* @extends EngineObject3D
|
|
29411
29543
|
* @memberof Render3D
|
|
@@ -29435,48 +29567,62 @@ class LensFlare3D extends EngineObject3D
|
|
|
29435
29567
|
this.saturation = saturation;
|
|
29436
29568
|
/** @property {number} - Picks the arrangement of the ghosts, another seed is another flare */
|
|
29437
29569
|
this.seed = 1;
|
|
29438
|
-
/** @property {Array<
|
|
29439
|
-
*
|
|
29440
|
-
*
|
|
29441
|
-
*
|
|
29442
|
-
|
|
29570
|
+
/** @property {Array<string>|undefined} - The shapes the ghosts are picked from, glow, disc, ring, hex,
|
|
29571
|
+
* streak or star: ['hex'] makes every ghost a hexagon, and a shape listed twice is picked twice as often;
|
|
29572
|
+
* discs, rings and glows when not set
|
|
29573
|
+
* @type {Array<string>|undefined} */
|
|
29574
|
+
this.shapes = undefined;
|
|
29575
|
+
/** @property {number} - Scales the glow at the sun, 0 for none */
|
|
29576
|
+
this.glowSize = 1;
|
|
29577
|
+
/** @property {number} - Scales the ghosts */
|
|
29578
|
+
this.ghostSize = 1;
|
|
29579
|
+
/** @property {Array<LensFlareElement>|undefined} - The parts of the flare, to set your own in place of the
|
|
29580
|
+
* ones made from count, seed, saturation, shapes, glowSize and ghostSize
|
|
29581
|
+
* @type {Array<LensFlareElement>|undefined} */
|
|
29443
29582
|
this.elements = undefined;
|
|
29444
29583
|
/** @property {Light3D|undefined} - A light the flare is of in place of the sun, a lamp or a spotlight: the
|
|
29445
29584
|
* flare is at the light and in its color, smaller from farther than the light reaches, hidden by what is
|
|
29446
|
-
* in front of the light,
|
|
29585
|
+
* in front of the light but its lamp, a mesh around it no wider than half its radius, and a spotlight's
|
|
29586
|
+
* shows from inside its beam only; a DirectionalLight3D's is far
|
|
29587
|
+
* away where it shines from, like the sun's; the flare is destroyed when its light is; light.addFlare
|
|
29588
|
+
* sets this
|
|
29447
29589
|
* @type {Light3D|undefined} */
|
|
29448
29590
|
this.light = undefined;
|
|
29449
29591
|
/** @property {boolean} - Fade out when something is between the camera and the sun */
|
|
29450
29592
|
this.occlusion = true;
|
|
29451
29593
|
/** @property {number} - Seconds the flare takes to fade out or in when the sun is hidden or shows again */
|
|
29452
29594
|
this.fadeTime = .15;
|
|
29453
|
-
/** @property {number} - How much of the sun shows, 0 hidden or behind the camera to 1 in
|
|
29595
|
+
/** @property {number} - How much of the sun shows, 0 hidden, off the screen or behind the camera to 1 in
|
|
29596
|
+
* plain view, eased */
|
|
29454
29597
|
this.visible = 1;
|
|
29455
29598
|
this.renderOrder = 1e9; // over the game's sprites, it is light in the lens
|
|
29456
29599
|
this.madeKey = '';
|
|
29457
|
-
/** @type {Array<
|
|
29600
|
+
/** @type {Array<LensFlareElement>} */
|
|
29458
29601
|
this.made = [];
|
|
29459
29602
|
}
|
|
29460
29603
|
|
|
29461
|
-
/** The parts of the flare: the elements set by hand, or the ones made from count, seed
|
|
29462
|
-
* and a core at the sun and the ghosts, made again when one of those changes
|
|
29463
|
-
* @return {Array<
|
|
29604
|
+
/** The parts of the flare: the elements set by hand, or the ones made from count, seed, saturation, shapes,
|
|
29605
|
+
* glowSize and ghostSize, a glow and a core at the sun and the ghosts, made again when one of those changes
|
|
29606
|
+
* @return {Array<LensFlareElement>} */
|
|
29464
29607
|
getElements()
|
|
29465
29608
|
{
|
|
29466
29609
|
if (this.elements) return this.elements;
|
|
29467
|
-
const
|
|
29610
|
+
const shapes = this.shapes?.length ? this.shapes : undefined, glow = this.glowSize, ghost = this.ghostSize;
|
|
29611
|
+
const key = [this.count, this.seed, this.saturation, glow, ghost, shapes?.length, shapes].join();
|
|
29468
29612
|
if (key !== this.madeKey)
|
|
29469
29613
|
{
|
|
29470
29614
|
const random = new RandomGenerator(this.seed * 7919 + 1), s = clamp(this.saturation);
|
|
29471
|
-
|
|
29472
|
-
|
|
29473
|
-
{at: 0, size: .
|
|
29615
|
+
/** @type {Array<LensFlareElement>} */
|
|
29616
|
+
const made = this.made = glow > 0 ? [
|
|
29617
|
+
{at: 0, size: .7 * glow, color: hsl(0, 0, 1, .5), shape: 'glow'},
|
|
29618
|
+
{at: 0, size: .25 * glow, color: hsl(0, 0, 1, .9), shape: 'glow'}] : [];
|
|
29474
29619
|
for (let i = 0; i < this.count; ++i)
|
|
29475
29620
|
{
|
|
29476
29621
|
// spread along the line, each a place of its own, the far ones bigger
|
|
29477
29622
|
const at = (i + random.float(.2, .8)) / max(this.count, 1) * 1.9 + .2;
|
|
29478
|
-
const pick = random.float(), shape = pick
|
|
29479
|
-
|
|
29623
|
+
const pick = random.float(), shape = shapes ? shapes[min(pick * shapes.length | 0, shapes.length - 1)] :
|
|
29624
|
+
pick < .5 ? 'disc' : pick < .8 ? 'ring' : 'glow';
|
|
29625
|
+
made.push({at, size: random.float(.04, .1) * (1 + at) * ghost, shape,
|
|
29480
29626
|
color: hsl(random.float(), s * .9, .6, random.float(.15, .4))});
|
|
29481
29627
|
}
|
|
29482
29628
|
this.madeKey = key;
|
|
@@ -29484,69 +29630,85 @@ class LensFlare3D extends EngineObject3D
|
|
|
29484
29630
|
return this.made;
|
|
29485
29631
|
}
|
|
29486
29632
|
|
|
29487
|
-
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun
|
|
29488
|
-
//
|
|
29633
|
+
// what the flare is of, seen from the camera: the way to it, how far it is, Infinity for the sun and for a
|
|
29634
|
+
// directional light, which shines from its place toward the origin, and a point to find it on the screen by;
|
|
29635
|
+
// undefined with no direction, or a light that is gone or at the camera
|
|
29489
29636
|
flareSource()
|
|
29490
29637
|
{
|
|
29491
29638
|
const camera = render3D.camera.pos, light = this.light;
|
|
29492
|
-
if (
|
|
29639
|
+
if (light?.destroyed) return;
|
|
29640
|
+
if (!light || light.directional)
|
|
29493
29641
|
{
|
|
29494
|
-
const
|
|
29495
|
-
if (!
|
|
29496
|
-
const direction =
|
|
29642
|
+
const from = light ? light.getWorldPos3D() : render3D.sunDirection;
|
|
29643
|
+
if (!from.lengthSquared()) return;
|
|
29644
|
+
const direction = from.normalize();
|
|
29497
29645
|
return {direction, distance: Infinity, pos: camera.add(direction.scale(100))};
|
|
29498
29646
|
}
|
|
29499
|
-
if (light.destroyed) return;
|
|
29500
29647
|
const pos = light.getWorldPos3D(), offset = pos.subtract(camera), distance = offset.length();
|
|
29501
29648
|
return distance ? {direction: offset.scale(1 / distance), distance, pos} : undefined;
|
|
29502
29649
|
}
|
|
29503
29650
|
|
|
29504
|
-
|
|
29505
|
-
|
|
29506
|
-
|
|
29507
|
-
|
|
29651
|
+
// how the flare would show with nothing in the way: where its source is on the screen, how strong it is there,
|
|
29652
|
+
// fading as it leaves the screen, its tint and the height its sizes are parts of; undefined when it would not
|
|
29653
|
+
// show at all, behind the camera, off the screen, or of a light that is off or seen from outside its cone
|
|
29654
|
+
flareLook()
|
|
29508
29655
|
{
|
|
29509
|
-
const source = this.flareSource();
|
|
29510
|
-
|
|
29511
|
-
|
|
29512
|
-
|
|
29513
|
-
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels and its
|
|
29514
|
-
* color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
29515
|
-
* @return {Array<{pos: Vector2, size: number, color: Color, shape: string}>} */
|
|
29516
|
-
getScreenElements()
|
|
29517
|
-
{
|
|
29518
|
-
const sun = this.getSunScreenPos(), center = mainCanvasSize.scale(.5);
|
|
29519
|
-
if (!sun || !center.x || !center.y) return [];
|
|
29656
|
+
const source = this.flareSource(), center = mainCanvasSize.scale(.5);
|
|
29657
|
+
const sun = source && render3D.worldToScreen(source.pos);
|
|
29658
|
+
if (!sun || !center.x || !center.y) return;
|
|
29520
29659
|
// it fades as the sun leaves the screen, gone when it is a third of the screen past the edge
|
|
29521
29660
|
const off = max(abs(sun.x - center.x) / center.x, abs(sun.y - center.y) / center.y);
|
|
29522
|
-
const strength =
|
|
29523
|
-
if (!(strength > 0)) return
|
|
29661
|
+
const strength = clamp((1.3 - off) / .5) * this.intensity;
|
|
29662
|
+
if (!(strength > 0)) return;
|
|
29524
29663
|
let tint = this.color.multiply(render3D.sunColor), height = mainCanvasSize.y * this.flareSize;
|
|
29525
|
-
const light = this.light
|
|
29664
|
+
const light = this.light;
|
|
29526
29665
|
if (light)
|
|
29527
29666
|
{
|
|
29528
|
-
if (!source) return [];
|
|
29529
29667
|
// a light's flare is the light's color, as bright as the light up to 1, and only from inside its cone
|
|
29530
29668
|
const cone = render3DLightCone(light), d = source.direction;
|
|
29531
29669
|
const inCone = clamp(-(cone[0] * d.x + cone[1] * d.y + cone[2] * d.z) - cone[3]);
|
|
29532
29670
|
const c = light.color, bright = c.a * clamp(light.intensity) * inCone;
|
|
29533
|
-
if (!(bright > 0)) return
|
|
29671
|
+
if (!(bright > 0)) return;
|
|
29534
29672
|
tint = this.color.multiply(rgb(c.r, c.g, c.b, bright));
|
|
29535
|
-
|
|
29673
|
+
if (!light.directional)
|
|
29674
|
+
height *= min(1, light.radius / source.distance); // smaller from farther than it reaches
|
|
29536
29675
|
}
|
|
29676
|
+
return height > 0 ? {sun, center, strength, tint, height} : undefined;
|
|
29677
|
+
}
|
|
29678
|
+
|
|
29679
|
+
/** Where the sun, or the flare's light, is on the screen, in pixels like mousePosScreen, undefined when it is
|
|
29680
|
+
* behind the camera
|
|
29681
|
+
* @return {Vector2|undefined} */
|
|
29682
|
+
getScreenPos()
|
|
29683
|
+
{
|
|
29684
|
+
const source = this.flareSource();
|
|
29685
|
+
return source && render3D.worldToScreen(source.pos);
|
|
29686
|
+
}
|
|
29687
|
+
|
|
29688
|
+
/** The parts of the flare as they are drawn now: each one's place on the screen, its size in pixels, a vector
|
|
29689
|
+
* when the element's is, and its color, dimmed by how much of the sun shows; empty when there is nothing to draw
|
|
29690
|
+
* @return {Array<{pos: Vector2, size: number|Vector2, color: Color, shape: string|undefined,
|
|
29691
|
+
* tileInfo: TileInfo|undefined, angle: number}>} */
|
|
29692
|
+
getScreenElements()
|
|
29693
|
+
{
|
|
29694
|
+
const look = this.visible > 0 && this.flareLook();
|
|
29695
|
+
if (!look) return [];
|
|
29696
|
+
const {sun, center, tint, height} = look, strength = look.strength * this.visible;
|
|
29537
29697
|
return this.getElements().map((e)=>
|
|
29538
29698
|
{
|
|
29539
29699
|
const c = e.color.multiply(tint);
|
|
29540
29700
|
// along the line and past the middle, a lerp would stop there
|
|
29541
|
-
|
|
29542
|
-
|
|
29701
|
+
const size = typeof e.size == 'number' ? e.size * height : e.size.scale(height);
|
|
29702
|
+
return {pos: sun.add(center.subtract(sun).scale(e.at)), size, shape: e.shape, tileInfo: e.tileInfo,
|
|
29703
|
+
angle: e.angle || 0, color: rgb(c.r, c.g, c.b, c.a * strength)};
|
|
29543
29704
|
});
|
|
29544
29705
|
}
|
|
29545
29706
|
|
|
29546
29707
|
/** Is something between the camera and the sun, or the flare's light: the level, or an object that is not see
|
|
29547
|
-
* through
|
|
29708
|
+
* through, hit on its triangles as render3D.pick hits it, so a mesh the camera is inside hides nothing unless
|
|
29709
|
+
* it is doubleSided
|
|
29548
29710
|
* @return {boolean} */
|
|
29549
|
-
|
|
29711
|
+
isHidden()
|
|
29550
29712
|
{
|
|
29551
29713
|
const source = this.flareSource();
|
|
29552
29714
|
if (!source) return true;
|
|
@@ -29560,22 +29722,31 @@ class LensFlare3D extends EngineObject3D
|
|
|
29560
29722
|
for (const map of maps)
|
|
29561
29723
|
if (map.raycast(ray, reach, (type)=> !map.blockType(type).seeThrough))
|
|
29562
29724
|
return true;
|
|
29563
|
-
//
|
|
29564
|
-
//
|
|
29565
|
-
//
|
|
29566
|
-
const
|
|
29567
|
-
|
|
29568
|
-
|
|
29569
|
-
|
|
29570
|
-
|
|
29725
|
+
// a light's lamp, the mesh the light is inside, does not hide it: one whose box, in its own space, the light
|
|
29726
|
+
// is in, and no wider than half the light's reach, since a room or a whole level has a box around its lights
|
|
29727
|
+
// too, and its walls do hide them from outside
|
|
29728
|
+
const lamp = (o)=>
|
|
29729
|
+
{
|
|
29730
|
+
if (!this.light || reach === Infinity || !o.mesh || o instanceof HeightMap) return false;
|
|
29731
|
+
const matrix = render3DObjectMatrix(o), b = o.mesh.bounds || o.mesh.getBounds();
|
|
29732
|
+
const side = max(b.max.x - b.min.x, b.max.y - b.min.y, b.max.z - b.min.z) * render3DMaxStretch(matrix.m);
|
|
29733
|
+
if (!matrix.determinant() || side > this.light.radius / 2) return false;
|
|
29734
|
+
const p = matrix.copy().invert().transformPoint(source.pos);
|
|
29735
|
+
return p.x >= b.min.x && p.x <= b.max.x && p.y >= b.min.y && p.y <= b.max.y &&
|
|
29736
|
+
p.z >= b.min.z && p.z <= b.max.z;
|
|
29737
|
+
};
|
|
29738
|
+
const hit = render3D.pick(ray, blockers.filter((o)=> !maps.some((map)=> map === o) && !lamp(o)));
|
|
29571
29739
|
return !!hit && hit.distance < reach;
|
|
29572
29740
|
}
|
|
29573
29741
|
|
|
29574
|
-
/** Ease visible toward whether the sun shows, called automatically each frame
|
|
29742
|
+
/** Ease visible toward whether the sun shows, called automatically each frame; a flare that would not show
|
|
29743
|
+
* wherever the sun is, off the screen or its light off, looks for nothing in the way */
|
|
29575
29744
|
update()
|
|
29576
29745
|
{
|
|
29577
29746
|
super.update();
|
|
29578
|
-
|
|
29747
|
+
if (this.light?.destroyed)
|
|
29748
|
+
return this.destroy(); // the flare of a light that is gone
|
|
29749
|
+
const shows = !!this.flareLook() && !(this.occlusion && this.isHidden());
|
|
29579
29750
|
const step = this.fadeTime > 0 ? timeDelta / this.fadeTime : 1;
|
|
29580
29751
|
this.visible = clamp(this.visible + (shows ? step : -step));
|
|
29581
29752
|
}
|
|
@@ -29589,8 +29760,9 @@ class LensFlare3D extends EngineObject3D
|
|
|
29589
29760
|
setAdditiveBlendMode(true);
|
|
29590
29761
|
for (const e of elements)
|
|
29591
29762
|
{
|
|
29592
|
-
const tileInfo = render3DFlareTile(e.shape);
|
|
29593
|
-
|
|
29763
|
+
const tileInfo = e.tileInfo || render3DFlareTile(e.shape);
|
|
29764
|
+
const size = typeof e.size == 'number' ? vec2(e.size) : e.size;
|
|
29765
|
+
tileInfo && drawTile(e.pos, size, tileInfo, e.color, e.angle, false, undefined, true, true);
|
|
29594
29766
|
}
|
|
29595
29767
|
setAdditiveBlendMode(false);
|
|
29596
29768
|
}
|
|
@@ -30196,7 +30368,7 @@ class VoxelMap extends EngineObject3D
|
|
|
30196
30368
|
/** @type {Array<Vector3>} */
|
|
30197
30369
|
this.chunkCenters = []; // a chunk mesh's points are around its center, which it is drawn at
|
|
30198
30370
|
this.chunksChanged = new Set;
|
|
30199
|
-
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean}|undefined>} */
|
|
30371
|
+
/** @type {Array<{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}|undefined>} */
|
|
30200
30372
|
this.blockTypes = [];
|
|
30201
30373
|
this.tiles = new Map; // tile index to the uvs of its corners
|
|
30202
30374
|
|
|
@@ -30268,27 +30440,29 @@ class VoxelMap extends EngineObject3D
|
|
|
30268
30440
|
* @param {number} type - 1 to 255
|
|
30269
30441
|
* @param {number|Array<number>|{top?: number, side: number, bottom?: number}} faces - A tile index for every face,
|
|
30270
30442
|
* 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
|
|
30271
|
-
* @param {{seeThrough?: boolean, transparent?: boolean}} [options] - seeThrough for holes
|
|
30272
|
-
* leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
30273
|
-
* transparent stage, and see-through too
|
|
30274
|
-
|
|
30443
|
+
* @param {{seeThrough?: boolean, transparent?: boolean, doubleSided?: boolean}} [options] - seeThrough for holes
|
|
30444
|
+
* in its texture, like leaves, so the blocks beside it keep their faces; transparent to blend, like glass or
|
|
30445
|
+
* water, drawn in the transparent stage, and see-through too; doubleSided for faces seen from inside the
|
|
30446
|
+
* block as well, like water, whose surface then shows from under it */
|
|
30447
|
+
setBlockType(type, faces, {seeThrough=false, transparent=false, doubleSided=false}={})
|
|
30275
30448
|
{
|
|
30276
30449
|
ASSERT(type >= 1 && type <= 255 && type % 1 === 0, 'a block type is a whole number from 1 to 255', type);
|
|
30277
30450
|
const f = /** @type {any} */ (faces);
|
|
30278
30451
|
const list = isNumber(f) ? [f, f, f, f, f, f] : isArray(f) ? f :
|
|
30279
30452
|
[f.side, f.side, f.top ?? f.side, f.bottom ?? f.side, f.side, f.side];
|
|
30280
30453
|
ASSERT(list.length === 6 && list.every(isNumber), 'faces is a tile index, six of them, or {top, side, bottom}');
|
|
30281
|
-
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent};
|
|
30454
|
+
this.blockTypes[type] = {faces: list, seeThrough: seeThrough || transparent, transparent, doubleSided};
|
|
30282
30455
|
this.rebuild();
|
|
30283
30456
|
}
|
|
30284
30457
|
|
|
30285
30458
|
/** A block type's faces and how it is seen through
|
|
30286
30459
|
* @param {number} type
|
|
30287
|
-
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean}}
|
|
30460
|
+
* @return {{faces: Array<number>, seeThrough: boolean, transparent: boolean, doubleSided: boolean}}
|
|
30288
30461
|
* @ignore */
|
|
30289
30462
|
blockType(type)
|
|
30290
30463
|
{
|
|
30291
|
-
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false
|
|
30464
|
+
return this.blockTypes[type] ||= {faces: [type, type, type, type, type, type], seeThrough: false, transparent: false,
|
|
30465
|
+
doubleSided: false};
|
|
30292
30466
|
}
|
|
30293
30467
|
|
|
30294
30468
|
/** Build every chunk again, after changing data directly or ambientOcclusion */
|
|
@@ -30358,6 +30532,11 @@ class VoxelMap extends EngineObject3D
|
|
|
30358
30532
|
colors[k] = ao ? RENDER3D_VOXEL_SHADES[shade[i]] : WHITE;
|
|
30359
30533
|
}
|
|
30360
30534
|
mesh.addStrip(points, normal, uvs, colors);
|
|
30535
|
+
|
|
30536
|
+
// a double sided block shows the face from inside too, its corners the other way round
|
|
30537
|
+
if (block.doubleSided)
|
|
30538
|
+
mesh.addStrip([points[1], points[0], points[3], points[2]], normal.scale(-1),
|
|
30539
|
+
[uvs[1], uvs[0], uvs[3], uvs[2]], [colors[1], colors[0], colors[3], colors[2]]);
|
|
30361
30540
|
}
|
|
30362
30541
|
}
|
|
30363
30542
|
this.chunkMeshes[index]?.dispose();
|
|
@@ -30704,8 +30883,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
30704
30883
|
* are added before its objects are made, one the game added itself keeps its place
|
|
30705
30884
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
30706
30885
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
30707
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and
|
|
30708
|
-
* leaves out stays as the game set it, and a level with no block changes
|
|
30886
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
|
|
30887
|
+
* sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
|
|
30888
|
+
* nothing
|
|
30709
30889
|
* @param {Object} level - The level, the level editor edits this same object
|
|
30710
30890
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
30711
30891
|
* @memberof Level3D */
|
|
@@ -30930,7 +31110,24 @@ function level3DSceneApply(scene)
|
|
|
30930
31110
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
30931
31111
|
if (typeof scene.shadows === 'boolean')
|
|
30932
31112
|
r.shadows = scene.shadows;
|
|
31113
|
+
if (typeof scene.lensFlare === 'boolean')
|
|
31114
|
+
level3DSceneFlare(scene.lensFlare);
|
|
31115
|
+
}
|
|
31116
|
+
|
|
31117
|
+
// the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
|
|
31118
|
+
// earlier level's, and off takes away the one a scene made, never the game's
|
|
31119
|
+
let level3DSunFlare;
|
|
31120
|
+
function level3DSceneFlare(on)
|
|
31121
|
+
{
|
|
31122
|
+
if (!on)
|
|
31123
|
+
{
|
|
31124
|
+
level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
|
|
31125
|
+
level3DSunFlare = undefined;
|
|
31126
|
+
}
|
|
31127
|
+
else if (!level3DSunHasFlare())
|
|
31128
|
+
level3DSunFlare = new LensFlare3D;
|
|
30933
31129
|
}
|
|
31130
|
+
const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
|
|
30934
31131
|
|
|
30935
31132
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
30936
31133
|
function level3DVector(value, fallback)
|
|
@@ -31280,7 +31477,8 @@ level3DAddType('Cylinder', function(pos, properties)
|
|
|
31280
31477
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
31281
31478
|
{color: WHITE, tile: -1, solid: true});
|
|
31282
31479
|
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
31283
|
-
// object's rotation aims it, softness is how much of the cone fades,
|
|
31480
|
+
// object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
|
|
31481
|
+
// lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
|
|
31284
31482
|
level3DAddType('Light', function(pos, properties)
|
|
31285
31483
|
{
|
|
31286
31484
|
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
@@ -31288,8 +31486,9 @@ level3DAddType('Light', function(pos, properties)
|
|
|
31288
31486
|
light.coneSoftness = properties.softness;
|
|
31289
31487
|
if (properties.shadows && render3D)
|
|
31290
31488
|
render3D.shadowLight = light;
|
|
31489
|
+
properties.lensFlare && light.addFlare();
|
|
31291
31490
|
return light;
|
|
31292
|
-
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
31491
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
|
|
31293
31492
|
|
|
31294
31493
|
/**
|
|
31295
31494
|
* LittleJS Particle Effects Plugin
|
|
@@ -34128,8 +34327,8 @@ function editorMapRestore(map)
|
|
|
34128
34327
|
if ((saved.width ?? map.width) === map.width && (saved.height ?? map.height) === map.height &&
|
|
34129
34328
|
editorSameData(saved.layers, data) && editorSameData(saved.objects ?? [], objects))
|
|
34130
34329
|
editorDiscardPending(record);
|
|
34131
|
-
else if (!sameFile || !
|
|
34132
|
-
!editorCopyData(data, saved.layers))
|
|
34330
|
+
else if (!sameFile || !editorSavedFits(saved, data.length, saved.width ?? map.width, saved.height ?? map.height) ||
|
|
34331
|
+
!editorResizeMap(map, saved.width ?? map.width, saved.height ?? map.height) || !editorCopyData(data, saved.layers))
|
|
34133
34332
|
{
|
|
34134
34333
|
record.pending = saved;
|
|
34135
34334
|
console.warn(`LittleJS editor: ${record.fileName} changed since its autosaved edits, ` +
|
|
@@ -34428,6 +34627,11 @@ function editorCopyData(data, saved)
|
|
|
34428
34627
|
return true;
|
|
34429
34628
|
}
|
|
34430
34629
|
|
|
34630
|
+
// 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
|
|
34631
|
+
// that fit it
|
|
34632
|
+
const editorSavedFits = (saved, count, width, height)=> isArray(saved.layers) && saved.layers.length === count &&
|
|
34633
|
+
saved.layers.every((layer)=> isArray(layer) && layer.length === width * height);
|
|
34634
|
+
|
|
34431
34635
|
// remember a map's tile data and objects, or forget them when they are back to the file
|
|
34432
34636
|
function editorAutosave(record)
|
|
34433
34637
|
{
|
|
@@ -34456,7 +34660,7 @@ function editorPaintData(record, data, objects, width=record.map.width, height=r
|
|
|
34456
34660
|
{
|
|
34457
34661
|
if (!editorHas('onRestart')) return;
|
|
34458
34662
|
const before = editorMapSnapshot(record);
|
|
34459
|
-
editorSetMapSnapshot(record, {width, height, layers: data, objects});
|
|
34663
|
+
editorSetMapSnapshot(record, {width, height, layers: data, objects}, keepUnknown);
|
|
34460
34664
|
editorMapChanged(record, before);
|
|
34461
34665
|
return;
|
|
34462
34666
|
}
|
|
@@ -34573,13 +34777,15 @@ function editorMapSnapshot(record)
|
|
|
34573
34777
|
return editorObjectsCopy({width: map.width, height: map.height, layers: editorTileLayerData(map.layers), objects});
|
|
34574
34778
|
}
|
|
34575
34779
|
|
|
34576
|
-
// give a map a size, tiles and objects, the game's Restart hook makes its layers again
|
|
34577
|
-
|
|
34780
|
+
// give a map a size, tiles and objects, the game's Restart hook makes its layers again; keepUnknown leaves an object
|
|
34781
|
+
// layer past the snapshot's as it is, as applying an autosave from before the file had it does
|
|
34782
|
+
function editorSetMapSnapshot(record, snapshot, keepUnknown=false)
|
|
34578
34783
|
{
|
|
34579
34784
|
const map = record.map;
|
|
34580
34785
|
editorResizeMap(map, snapshot.width, snapshot.height);
|
|
34581
34786
|
editorCopyData(editorTileLayerData(map.layers), snapshot.layers);
|
|
34582
34787
|
editorRestoreObjects(map, snapshot);
|
|
34788
|
+
if (!keepUnknown)
|
|
34583
34789
|
for (const group of editorObjectGroups(map.layers).slice(snapshot.objects.length))
|
|
34584
34790
|
group.objects = []; // an Objects layer the editor made since
|
|
34585
34791
|
editorClearSelections();
|
|
@@ -35739,9 +35945,13 @@ function editorPaletteTiles(layer)
|
|
|
35739
35945
|
if (!image) return [];
|
|
35740
35946
|
if (layer.palette?.image === image) return layer.palette.tiles;
|
|
35741
35947
|
|
|
35742
|
-
|
|
35948
|
+
// a sheet read by its columns, a tileset with a margin or a spacing, is read with frame(), which asserts the
|
|
35949
|
+
// frame is inside the image, so a row past the image's end is found before its frame is made
|
|
35950
|
+
const tiles = [], base = live.tileInfo, columns = base.columns;
|
|
35743
35951
|
for (let i = 0; i < 4096; ++i)
|
|
35744
35952
|
{
|
|
35953
|
+
if (columns && base.pos.y + (i / columns | 0) * (base.size.y + base.padding*2) + base.size.y > image.height)
|
|
35954
|
+
break;
|
|
35745
35955
|
const t = editorTileInfo(live, i);
|
|
35746
35956
|
if (t.pos.x + t.size.x > image.width || t.pos.y + t.size.y > image.height) break;
|
|
35747
35957
|
tiles.push({tile: i, tileInfo: t});
|
|
@@ -36835,7 +37045,7 @@ function editor3DSceneState()
|
|
|
36835
37045
|
const r = render3D;
|
|
36836
37046
|
return {sky: r.sky, ambientColor: r.ambientColor.copy(), ambientGroundColor: r.ambientGroundColor?.copy(),
|
|
36837
37047
|
fogStart: r.fogStart, fogEnd: r.fogEnd, fogColor: r.fogColor?.copy(), sunDirection: r.sunDirection.copy(),
|
|
36838
|
-
sunColor: r.sunColor.copy(), shadows: r.shadows};
|
|
37048
|
+
sunColor: r.sunColor.copy(), shadows: r.shadows, sunFlare: !!level3DSunFlare && !level3DSunFlare.destroyed};
|
|
36839
37049
|
}
|
|
36840
37050
|
function editor3DSceneRestore(state)
|
|
36841
37051
|
{
|
|
@@ -36850,6 +37060,7 @@ function editor3DSceneRestore(state)
|
|
|
36850
37060
|
r.sunDirection = state.sunDirection.copy();
|
|
36851
37061
|
r.sunColor = state.sunColor.copy();
|
|
36852
37062
|
r.shadows = state.shadows;
|
|
37063
|
+
level3DSceneFlare(state.sunFlare); // a flare an earlier level's scene gave the sun, never the game's own
|
|
36853
37064
|
}
|
|
36854
37065
|
|
|
36855
37066
|
// the scene on screen as a block, to start a level's scene from: a sky only when the dome's colors are known
|
|
@@ -36871,6 +37082,7 @@ function editor3DSceneFromView()
|
|
|
36871
37082
|
scene.fog = [editor3DRound(r.fogStart || 0), editor3DRound(r.fogEnd || 0)];
|
|
36872
37083
|
scene.fogColor = editor3DHex(r.fogColor || canvasClearColor);
|
|
36873
37084
|
scene.shadows = !!r.shadows;
|
|
37085
|
+
scene.lensFlare = level3DSunHasFlare();
|
|
36874
37086
|
return scene;
|
|
36875
37087
|
}
|
|
36876
37088
|
|
|
@@ -37028,9 +37240,11 @@ function editor3DLevelLoaded(level)
|
|
|
37028
37240
|
level.objects = editor3DCopy(saved.objects);
|
|
37029
37241
|
for (const name of editor3DLevelPartNames)
|
|
37030
37242
|
parts[name] ? level[name] = editor3DCopy(parts[name]) : delete level[name];
|
|
37243
|
+
saved.stale && console.warn(`LittleJS editor: the edits brought back to ${record.fileName} are older than ` +
|
|
37244
|
+
'the last ones, which storage had no room for');
|
|
37031
37245
|
}
|
|
37032
|
-
else if (saved)
|
|
37033
|
-
record.pending = saved;
|
|
37246
|
+
else if (saved && isArray(saved.objects))
|
|
37247
|
+
record.pending = saved; // one that is broken is passed by, and the next autosave writes over it
|
|
37034
37248
|
}
|
|
37035
37249
|
|
|
37036
37250
|
// called by level3DLoad for each object, with what its type made
|
|
@@ -39859,6 +40073,8 @@ function editor3DSceneUpdate(box)
|
|
|
39859
40073
|
color('Fog color', 'The sky\'s horizon color when the level has a sky and no fog color',
|
|
39860
40074
|
scene.fogColor ?? (sky ? sky[1] : view.fogColor), (hex)=> change('fogColor', hex));
|
|
39861
40075
|
toggle('Shadows', 'The sun casts shadows', !!value('shadows'), (on)=> change('shadows', on));
|
|
40076
|
+
toggle('Lens flare', 'The sun flares in the lens when it is in view', !!value('lensFlare'), (on)=>
|
|
40077
|
+
change('lensFlare', on));
|
|
39862
40078
|
}
|
|
39863
40079
|
|
|
39864
40080
|
// the properties box: the position, rotation and scale of the one selected object and an input for each default
|
|
@@ -40418,6 +40634,7 @@ export
|
|
|
40418
40634
|
tileCollisionTest,
|
|
40419
40635
|
tileCollisionRaycast,
|
|
40420
40636
|
tileLayersLoad,
|
|
40637
|
+
tileLayersFromLDtk,
|
|
40421
40638
|
objectLayersAddType,
|
|
40422
40639
|
objectLayersLoad,
|
|
40423
40640
|
TileLayerData,
|
|
@@ -40476,10 +40693,6 @@ export
|
|
|
40476
40693
|
LightSystemPlugin,
|
|
40477
40694
|
Light,
|
|
40478
40695
|
|
|
40479
|
-
// ZzFXMusic
|
|
40480
|
-
ZzFXMusic,
|
|
40481
|
-
zzfxM,
|
|
40482
|
-
|
|
40483
40696
|
// Audio Effects
|
|
40484
40697
|
AudioEffect,
|
|
40485
40698
|
AudioFilter,
|