littlejsengine 1.24.1 → 1.24.2
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 +59 -1
- package/EDITOR.md +2 -0
- package/FAQ.md +13 -2
- package/README.md +1 -1
- package/REFERENCE.md +20 -6
- package/dist/littlejs.d.ts +77 -13
- package/dist/littlejs.esm.js +2395 -1939
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +2392 -1939
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2356 -1952
- package/package.json +1 -1
- package/plugins/gltf.js +62 -9
- package/plugins/lightSystem.js +1 -1
- package/plugins/math3d.js +1014 -1013
- package/plugins/medalSystem.js +7 -11
- package/plugins/particleEffects.js +2 -2
- package/plugins/pathFinder.js +2 -2
- package/plugins/pluginExport.js +1 -0
- package/plugins/render3d.js +131 -55
- package/plugins/render3dEditor.js +24 -10
- package/plugins/render3dExtras.js +29 -17
- package/plugins/render3dLevel.js +21 -12
- package/plugins/render3dVoxels.js +2 -2
- package/plugins/textureSheet.js +97 -3
- package/plugins/tweakables.js +1 -1
- package/plugins/tweenSystem.js +2 -2
- package/plugins/uiSystem.js +6 -2
- package/src/engine.js +34 -11
- package/src/engineAudio.js +13 -3
- package/src/engineDebug.js +4 -1
- package/src/engineDraw.js +29 -16
- package/src/engineEditor.js +82 -50
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +17 -3
- package/src/engineMath.js +6 -2
- package/src/engineObject.js +675 -675
- package/src/engineParticles.js +1 -1
- package/src/engineSettings.js +16 -0
- package/src/engineTileLayer.js +85 -22
- package/src/engineUtilities.js +20 -8
- package/src/engineWebGL.js +12 -8
package/src/engineParticles.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* rgb(1,1,1,0), rgb(0,0,0,0), // colorEndA, colorEndB
|
|
44
44
|
* 1, .2, .2, .1, .05, // particleTime, sizeStart, sizeEnd, particleSpeed, particleAngleSpeed
|
|
45
45
|
* .99, 1, 1, PI, .05, // damping, angleDamping, gravityScale, particleCone, fadeRate
|
|
46
|
-
* .5,
|
|
46
|
+
* .5, true // randomness, collide
|
|
47
47
|
* );
|
|
48
48
|
*/
|
|
49
49
|
class ParticleEmitter extends EngineObject
|
package/src/engineSettings.js
CHANGED
|
@@ -418,6 +418,13 @@ let vibrateEnable = true;
|
|
|
418
418
|
* @memberof Settings */
|
|
419
419
|
let soundEnable = true;
|
|
420
420
|
|
|
421
|
+
/** Play sound with an iPhone's silent switch on, as media does; off by default, so the switch mutes the game as it
|
|
422
|
+
* does a ringtone, and the player's own music keeps playing beside it; Safari 16.4 and up, elsewhere it does nothing
|
|
423
|
+
* @type {boolean}
|
|
424
|
+
* @default
|
|
425
|
+
* @memberof Settings */
|
|
426
|
+
let soundIgnoreSilentSwitch = false;
|
|
427
|
+
|
|
421
428
|
/** Volume scale to apply to all sound, music and speech
|
|
422
429
|
* Use setSoundVolume to also update the audio master gain immediately
|
|
423
430
|
* @type {number}
|
|
@@ -819,6 +826,15 @@ function setVibrateEnable(enable) { vibrateEnable = enable; }
|
|
|
819
826
|
* @memberof Settings */
|
|
820
827
|
function setSoundEnable(enable) { soundEnable = enable; }
|
|
821
828
|
|
|
829
|
+
/** Set if sound plays with an iPhone's silent switch on, as media does, which also pauses the player's own music
|
|
830
|
+
* @param {boolean} ignore
|
|
831
|
+
* @memberof Settings */
|
|
832
|
+
function setSoundIgnoreSilentSwitch(ignore)
|
|
833
|
+
{
|
|
834
|
+
soundIgnoreSilentSwitch = ignore;
|
|
835
|
+
audioSetSession();
|
|
836
|
+
}
|
|
837
|
+
|
|
822
838
|
/** Set volume scale to apply to all sound, music and speech
|
|
823
839
|
* @param {number} volume
|
|
824
840
|
* @memberof Settings */
|
package/src/engineTileLayer.js
CHANGED
|
@@ -124,6 +124,19 @@ function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnl
|
|
|
124
124
|
// Tiled's flip flags, horizontal, vertical and diagonal as bits 2, 1 and 0, as [direction, mirror]
|
|
125
125
|
const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [1,1]];
|
|
126
126
|
|
|
127
|
+
// the tile info a layer's tile info gives a tile: a tile set's tile, a frame of a sheet read by its columns, or a
|
|
128
|
+
// tile of its grid; undefined past the end of a tile set, or of a sheet read by its columns, as values kept only for
|
|
129
|
+
// collision can be, which the level editor reads the same way
|
|
130
|
+
function tileLayerTileInfo(t, tile)
|
|
131
|
+
{
|
|
132
|
+
if (!t) return;
|
|
133
|
+
if (t.tiles) return t.tiles[tile];
|
|
134
|
+
if (!t.columns) return t.index(tile);
|
|
135
|
+
if (t.textureInfo && t.pos.y + (tile / t.columns | 0) * (t.size.y + t.padding*2) + t.size.y > t.textureInfo.size.y)
|
|
136
|
+
return;
|
|
137
|
+
return t.frame(tile);
|
|
138
|
+
}
|
|
139
|
+
|
|
127
140
|
/**
|
|
128
141
|
* Load tile layers from exported data
|
|
129
142
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
@@ -143,7 +156,8 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
143
156
|
* @param {number} [renderOrder] - Render order of the top layer
|
|
144
157
|
* @param {number|string} [collisionLayer] - Layer to use for collision if any, by its index or its name
|
|
145
158
|
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
146
|
-
* @return {Array<TileCollisionLayer>}
|
|
159
|
+
* @return {Array<TileCollisionLayer>} - It throws for a map that is not whole cells, or a layer whose tiles do not
|
|
160
|
+
* fill it, before it makes any layer
|
|
147
161
|
* @memberof TileLayers */
|
|
148
162
|
function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrder=0, collisionLayer, draw=true)
|
|
149
163
|
{
|
|
@@ -160,15 +174,20 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
160
174
|
// the editor, in debug builds, keeps the map as the source of its edits and brings back autosaved ones
|
|
161
175
|
tileMapData = editorMapRestore(tileMapData);
|
|
162
176
|
|
|
163
|
-
// validate the tile map data
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
// validate the tile map data: a size that is not whole cells, or tiles that do not fill it, is said in any build,
|
|
178
|
+
// since the layers and their loops are made from that size
|
|
179
|
+
const {width: mapWidth, height: mapHeight} = tileMapData;
|
|
180
|
+
if (!(Number.isInteger(mapWidth) && Number.isInteger(mapHeight) && mapWidth > 0 && mapHeight > 0))
|
|
181
|
+
throw new Error(`tileLayersLoad: a map is a whole number of cells across and down, not ${mapWidth} by ${mapHeight}`);
|
|
182
|
+
if (!isArray(tileMapData.layers) || !tileMapData.layers.length)
|
|
183
|
+
throw new Error('tileLayersLoad: a map has a list of layers');
|
|
166
184
|
|
|
167
185
|
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
168
186
|
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
169
187
|
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
188
|
+
// a tile set from loadTiles has each tile where it was packed, whatever the map says of its own image
|
|
170
189
|
const tileset = tileMapData.tilesets?.[0];
|
|
171
|
-
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
190
|
+
if (tileInfo && !tileInfo.tiles && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
172
191
|
{
|
|
173
192
|
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
174
193
|
const width = tileInfo.textureInfo?.size.x || 0;
|
|
@@ -210,20 +229,25 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
210
229
|
{
|
|
211
230
|
const name = collisionLayer;
|
|
212
231
|
collisionLayer = layers.findIndex((l)=> l.color && l.dataLayer.name === name);
|
|
213
|
-
|
|
232
|
+
collisionLayer < 0 && console.error('tileLayersLoad: no tile layer is named ' + name + ', none is solid');
|
|
214
233
|
}
|
|
215
234
|
|
|
216
|
-
// create tile layers and fill with data
|
|
235
|
+
// create tile layers and fill with data, every layer's tiles checked first, so a bad one leaves none made
|
|
217
236
|
const tileLayers = [];
|
|
218
237
|
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
219
238
|
const layerCount = layers.length;
|
|
239
|
+
for (const [layerIndex, {dataLayer, color}] of layers.entries())
|
|
240
|
+
{
|
|
241
|
+
const tiles = dataLayer.data; // a list of gids, or a typed array of them
|
|
242
|
+
if (color && (!(isArray(tiles) || ArrayBuffer.isView(tiles)) || dataLayer.data.length !== levelSize.area()))
|
|
243
|
+
throw new Error(`tileLayersLoad: layer ${dataLayer.name ?? layerIndex} has ${dataLayer.data?.length} tiles for a map of ` +
|
|
244
|
+
`${levelSize.area()}; infinite maps and compressed layers are not read`);
|
|
245
|
+
}
|
|
220
246
|
for (let layerIndex=layerCount; layerIndex--;)
|
|
221
247
|
{
|
|
222
248
|
const {dataLayer, color: layerColor, visible} = layers[layerIndex];
|
|
223
249
|
if (!layerColor)
|
|
224
250
|
continue;
|
|
225
|
-
ASSERT(dataLayer.data && dataLayer.data.length, 'tile layer has no data, infinite maps and compressed layers are not supported');
|
|
226
|
-
ASSERT(levelSize.area() === dataLayer.data.length);
|
|
227
251
|
|
|
228
252
|
const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
|
|
229
253
|
const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
|
|
@@ -232,8 +256,8 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
232
256
|
if (!visible)
|
|
233
257
|
tileLayer.render = ()=> {}; // a hidden layer keeps its tiles and collision but is not drawn
|
|
234
258
|
|
|
235
|
-
for (let x=levelSize.x; x
|
|
236
|
-
for (let y=levelSize.y; y
|
|
259
|
+
for (let x=0; x<levelSize.x; ++x)
|
|
260
|
+
for (let y=0; y<levelSize.y; ++y)
|
|
237
261
|
{
|
|
238
262
|
const pos = vec2(x, levelSize.y-1-y);
|
|
239
263
|
const data = dataLayer.data[x + y*levelSize.x];
|
|
@@ -305,6 +329,8 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
305
329
|
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
306
330
|
const grid = first?.__gridSize || 16;
|
|
307
331
|
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
332
|
+
if (!(width > 0 && height > 0 && width * height <= 1 << 22))
|
|
333
|
+
throw new Error('tileLayersFromLDtk: level ' + data.identifier + ' is ' + width + ' by ' + height + ' cells');
|
|
308
334
|
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
309
335
|
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
310
336
|
|
|
@@ -322,7 +348,7 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
322
348
|
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
323
349
|
if (!tiled(instance))
|
|
324
350
|
{
|
|
325
|
-
const objects = (instance.entityInstances || []).map((entity)=>
|
|
351
|
+
const objects = (instance.entityInstances || []).filter((entity)=> isArray(entity.px)).map((entity)=>
|
|
326
352
|
{
|
|
327
353
|
// an entity is placed by its pivot, an object here by its middle
|
|
328
354
|
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
@@ -338,12 +364,9 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
338
364
|
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
339
365
|
continue;
|
|
340
366
|
}
|
|
341
|
-
|
|
342
|
-
const ownTiles = !tiles.length || !tileset ||
|
|
343
|
-
instance.__tilesetDefUid === tileset.uid && tileset.tileGridSize === grid;
|
|
344
|
-
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height || !ownTiles)
|
|
367
|
+
if (instance.__gridSize !== grid || instance.__cWid !== width || instance.__cHei !== height)
|
|
345
368
|
{
|
|
346
|
-
|
|
369
|
+
console.warn(`tileLayersFromLDtk: layer ${name} has another grid, left out`);
|
|
347
370
|
continue;
|
|
348
371
|
}
|
|
349
372
|
// an IntGrid layer's values, hidden, under its own name, for collision whatever its tiles do
|
|
@@ -354,7 +377,20 @@ function tileLayersFromLDtk(ldtk, level=0)
|
|
|
354
377
|
const data = empty();
|
|
355
378
|
(instance.intGridCsv || []).forEach((value, k)=> k < data.length && (data[k] = value));
|
|
356
379
|
map.layers.push({id, name, type: 'tilelayer', width, height, data, opacity, visible: false, x: 0, y: 0});
|
|
357
|
-
|
|
380
|
+
}
|
|
381
|
+
// its tiles, those with a place, from the first layer's tileset
|
|
382
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])].filter((t)=> isArray(t.px));
|
|
383
|
+
if (!tiles.length)
|
|
384
|
+
{
|
|
385
|
+
// a tile layer with none yet is an empty layer, to paint in the editor, and a level of one still loads
|
|
386
|
+
values || map.layers.push({id, name, type: 'tilelayer', width, height, data: empty(), opacity,
|
|
387
|
+
visible: instance.visible !== false, x: 0, y: 0});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (tileset && (instance.__tilesetDefUid !== tileset.uid || tileset.tileGridSize !== grid))
|
|
391
|
+
{
|
|
392
|
+
console.warn(`tileLayersFromLDtk: the tiles of layer ${name} are on another tileset, left out`);
|
|
393
|
+
continue;
|
|
358
394
|
}
|
|
359
395
|
|
|
360
396
|
// a layer for each depth in a cell and each opacity, the first tiles drawn solid first
|
|
@@ -492,7 +528,15 @@ function objectLayersMake(tileMapData, object)
|
|
|
492
528
|
const pos = vec2(object.x / tilewidth, height - object.y / tileheight);
|
|
493
529
|
const {make} = type, result = make.prototype ? new make(pos) : make(pos);
|
|
494
530
|
if (!result || typeof result !== 'object') return;
|
|
495
|
-
|
|
531
|
+
for (const [key, value] of Object.entries(objectLayersProperties(type, object)))
|
|
532
|
+
{
|
|
533
|
+
// a property named as a method of the object, like update, would take the method's place, so it is left out
|
|
534
|
+
if (typeof result[key] === 'function' && typeof value !== 'function')
|
|
535
|
+
debug && console.warn(`objectLayersLoad: ${name} property ${key} has a method's name, left out`);
|
|
536
|
+
else
|
|
537
|
+
result[key] = value;
|
|
538
|
+
}
|
|
539
|
+
return result;
|
|
496
540
|
}
|
|
497
541
|
|
|
498
542
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -678,6 +722,7 @@ class TileLayer extends CanvasLayer
|
|
|
678
722
|
// set tile info
|
|
679
723
|
this.tileInfo = tileInfo.frame(0);
|
|
680
724
|
this.tileInfo.bleed = 0; // disable bleed for tile layers
|
|
725
|
+
this.tileInfo.tiles = tileInfo.tiles; // a tile set's list, filled in as its images load
|
|
681
726
|
}
|
|
682
727
|
|
|
683
728
|
// init tile data
|
|
@@ -857,8 +902,10 @@ class TileLayer extends CanvasLayer
|
|
|
857
902
|
const d = this.getData(layerPos);
|
|
858
903
|
if (!d || d.tile === undefined) return;
|
|
859
904
|
|
|
860
|
-
// a
|
|
861
|
-
|
|
905
|
+
// a tile set from loadTiles has each tile where it was packed, a tileset packed by loadSprite keeps its own
|
|
906
|
+
// columns, counted from its first tile, not the sheet's grid
|
|
907
|
+
const t = this.tileInfo, tileInfo = tileLayerTileInfo(t, d.tile);
|
|
908
|
+
if (t && !tileInfo) return; // past the end of the sheet or the tile set, or loading
|
|
862
909
|
this.drawLayerTile(drawPos, cellPixels, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
863
910
|
}
|
|
864
911
|
|
|
@@ -1133,7 +1180,23 @@ class TileCollisionLayer extends TileLayer
|
|
|
1133
1180
|
const tileData = this.getCollisionData(pos);
|
|
1134
1181
|
return tileData && collisionTest(tileData, vec2(pos.x + offset.x, pos.y + offset.y));
|
|
1135
1182
|
}
|
|
1136
|
-
|
|
1183
|
+
// only the part of the line over the layer, and a cell around it, can meet a tile, so a ray toward a point
|
|
1184
|
+
// far beyond is walked across the layer and no farther; the cell around keeps the step into the layer as it was
|
|
1185
|
+
const a = posStart.subtract(offset), d = posEnd.subtract(posStart);
|
|
1186
|
+
let t0 = 0, t1 = 1;
|
|
1187
|
+
for (const [p, q, high] of [[a.x, d.x, this.size.x + 1], [a.y, d.y, this.size.y + 1]])
|
|
1188
|
+
{
|
|
1189
|
+
if (!q)
|
|
1190
|
+
{
|
|
1191
|
+
if (p < -1 || p > high) return;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
const u = (-1 - p) / q, v = (high - p) / q;
|
|
1195
|
+
t0 = max(t0, min(u, v)), t1 = min(t1, max(u, v));
|
|
1196
|
+
}
|
|
1197
|
+
if (t0 > t1) return;
|
|
1198
|
+
const hitPos = lineTest(t0 ? a.add(d.scale(t0)) : a, t1 < 1 ? a.add(d.scale(t1)) : posEnd.subtract(offset),
|
|
1199
|
+
testFunction, normal);
|
|
1137
1200
|
if (hitPos)
|
|
1138
1201
|
hitPos.x += offset.x, hitPos.y += offset.y;
|
|
1139
1202
|
if (debugRaycast && hitPos)
|
package/src/engineUtilities.js
CHANGED
|
@@ -141,6 +141,7 @@ function saveText(text, filename='text', type='text/plain')
|
|
|
141
141
|
|
|
142
142
|
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
143
143
|
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
144
|
+
* - A browser with no OffscreenCanvas, Safari before 16.4, gets a canvas element that is not on the page
|
|
144
145
|
* @param {number} width - In pixels
|
|
145
146
|
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
146
147
|
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
@@ -149,9 +150,16 @@ function saveText(text, filename='text', type='text/plain')
|
|
|
149
150
|
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
150
151
|
{
|
|
151
152
|
ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
152
|
-
|
|
153
|
+
const canvas = typeof OffscreenCanvas != 'undefined' ? new OffscreenCanvas(width, height) :
|
|
154
|
+
Object.assign(document.createElement('canvas'), {width, height});
|
|
155
|
+
return /** @type {OffscreenCanvasRenderingContext2D} */ (canvas.getContext('2d', {willReadFrequently}));
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
// can a canvas be made to draw into: an OffscreenCanvas, or a canvas element where there is none, as in Safari before
|
|
159
|
+
// 16.4; neither in a headless test
|
|
160
|
+
function canvasAvailable()
|
|
161
|
+
{ return typeof OffscreenCanvas != 'undefined' || typeof document != 'undefined' && !!document.createElement; }
|
|
162
|
+
|
|
155
163
|
/** Save a canvas to disk
|
|
156
164
|
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
157
165
|
* @param {string} [filename]
|
|
@@ -159,7 +167,7 @@ function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
|
159
167
|
* @memberof Utilities */
|
|
160
168
|
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
161
169
|
{
|
|
162
|
-
if (canvas instanceof OffscreenCanvas)
|
|
170
|
+
if (typeof OffscreenCanvas != 'undefined' && canvas instanceof OffscreenCanvas)
|
|
163
171
|
{
|
|
164
172
|
// copy to temporary canvas and save
|
|
165
173
|
const saveCanvas = document.createElement('canvas');
|
|
@@ -169,7 +177,7 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
|
169
177
|
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
170
178
|
}
|
|
171
179
|
else
|
|
172
|
-
saveDataURL(canvas.toDataURL(type), filename);
|
|
180
|
+
saveDataURL(/** @type {HTMLCanvasElement} */ (canvas).toDataURL(type), filename);
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
/** Save a data url to disk
|
|
@@ -237,16 +245,19 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
237
245
|
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
/** Write save data to local storage
|
|
248
|
+
/** Write save data to local storage, an object as readSaveData gives it back
|
|
241
249
|
* @param {string} saveName - unique name for the game/save
|
|
242
250
|
* @param {object} saveData - object containing data to be saved
|
|
251
|
+
* @return {boolean} - Whether it was written, false when storage is unavailable or full
|
|
243
252
|
* @memberof Utilities */
|
|
244
253
|
function writeSaveData(saveName, saveData)
|
|
245
254
|
{
|
|
246
255
|
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
256
|
+
ASSERT(typeof saveData === 'object' && saveData !== null && !isArray(saveData),
|
|
257
|
+
'writeSaveData: save data must be an object, readSaveData reads it back into one');
|
|
247
258
|
// tolerate localStorage being unavailable or quota exceeded
|
|
248
|
-
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
249
|
-
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
259
|
+
try { localStorage.setItem(saveName, JSON.stringify(saveData)); return true; }
|
|
260
|
+
catch { LOG('writeSaveData: failed to write', saveName); return false; }
|
|
250
261
|
}
|
|
251
262
|
|
|
252
263
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -283,8 +294,9 @@ function noise2D(x, y)
|
|
|
283
294
|
{
|
|
284
295
|
const ix = floor(x), iy = floor(y);
|
|
285
296
|
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
286
|
-
// large prime decorrelates neighboring rows
|
|
287
|
-
|
|
297
|
+
// large prime decorrelates neighboring rows; multiplied as 32 bit integers, which is what the hash keeps of it,
|
|
298
|
+
// so the row stays exact however far out y is, where a plain product would lose its low bits
|
|
299
|
+
const h = (a, b) => noiseHash(a + Math.imul(b, 374761393));
|
|
288
300
|
return lerp(
|
|
289
301
|
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
290
302
|
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
package/src/engineWebGL.js
CHANGED
|
@@ -576,9 +576,9 @@ function glSetTextureData(texture, image, pixelated=tilesPixelated)
|
|
|
576
576
|
{
|
|
577
577
|
// WebGL ignores the unpack flag below for a bitmap and uploads it as it was made, so it is drawn onto a
|
|
578
578
|
// canvas first, which reads either kind of bitmap right and uploads as the flag says
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
image = canvas;
|
|
579
|
+
const context = createCanvasContext(image.width, image.height);
|
|
580
|
+
context.drawImage(image, 0, 0);
|
|
581
|
+
image = context.canvas;
|
|
582
582
|
}
|
|
583
583
|
// smooth filtering mixes a texel with its see through neighbors, right only for premultiplied color, or the
|
|
584
584
|
// edges go dark; pixel art is sampled a texel at a time, and uploads straight color as it always has
|
|
@@ -961,6 +961,9 @@ function glClearRect(x, y, width, height)
|
|
|
961
961
|
|
|
962
962
|
///////////////////////////////////////////////////////////////////////////////
|
|
963
963
|
|
|
964
|
+
// the strip glMakeOutline makes and the vectors it writes, kept for the next, as it is read into the batch first
|
|
965
|
+
const glOutlineStrip = [], glOutlinePool = [];
|
|
966
|
+
|
|
964
967
|
// WebGL internal function to convert polygon to outline triangle strip
|
|
965
968
|
function glMakeOutline(points, width, wrap=true)
|
|
966
969
|
{
|
|
@@ -968,7 +971,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
968
971
|
return [];
|
|
969
972
|
|
|
970
973
|
const halfWidth = width / 2;
|
|
971
|
-
const strip =
|
|
974
|
+
const strip = glOutlineStrip, pool = glOutlinePool;
|
|
975
|
+
strip.length = 0;
|
|
972
976
|
const n = points.length;
|
|
973
977
|
const e = 1e-6;
|
|
974
978
|
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
@@ -1025,10 +1029,10 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
1025
1029
|
}
|
|
1026
1030
|
|
|
1027
1031
|
// create inner and outer points along the normal
|
|
1028
|
-
const inner =
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
strip.push(outer);
|
|
1032
|
+
const inner = pool[strip.length] ||= vec2(), outer = pool[strip.length + 1] ||= vec2();
|
|
1033
|
+
inner.x = curr.x - nx * halfWidth, inner.y = curr.y - ny * halfWidth;
|
|
1034
|
+
outer.x = curr.x + nx * halfWidth, outer.y = curr.y + ny * halfWidth;
|
|
1035
|
+
strip.push(inner, outer);
|
|
1032
1036
|
}
|
|
1033
1037
|
if (strip.length > 1 && wrap)
|
|
1034
1038
|
{
|