littlejsengine 1.23.0 → 1.23.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/COPYRIGHT.txt +65 -38
- package/EDITOR.md +359 -0
- package/README.md +11 -6
- package/REFERENCE.md +2314 -0
- package/dist/littlejs.d.ts +10 -9
- package/dist/littlejs.esm.js +1405 -1234
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +1405 -1234
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1339 -1228
- package/package.json +4 -3
- package/plugins/gltf.js +29 -13
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +44 -14
- package/plugins/render3dEditor.js +46 -16
- package/plugins/render3dLevel.js +28 -2
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/plugins/zzfxm.js +161 -159
- package/src/engine.js +1 -1
- package/src/engineAudio.js +7 -1
- package/src/engineBuild.mjs +17 -2
- package/src/engineDraw.js +11 -7
- package/src/engineEditor.js +52 -18
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +5 -1
- package/src/engineTileLayer.js +2 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlejsengine",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "LittleJS - Tiny and Fast HTML5 Game Engine",
|
|
5
5
|
"main": "dist/littlejs.esm.js",
|
|
6
6
|
"types": "dist/littlejs.d.ts",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"plugins/",
|
|
40
40
|
"!plugins/box2d.wasm.*",
|
|
41
41
|
"COPYRIGHT.txt",
|
|
42
|
-
"FAQ.md"
|
|
42
|
+
"FAQ.md",
|
|
43
|
+
"REFERENCE.md",
|
|
44
|
+
"EDITOR.md"
|
|
43
45
|
],
|
|
44
46
|
"scripts": {
|
|
45
47
|
"build": "node src/engineBuild.mjs",
|
|
@@ -53,7 +55,6 @@
|
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"bestzip": "^2.2.1",
|
|
56
|
-
"clean-jsdoc-theme": "~4.2.9",
|
|
57
58
|
"electron": "^38.2.0",
|
|
58
59
|
"electron-packager": "^17.1.2",
|
|
59
60
|
"google-closure-compiler": "~20230502.0.0",
|
package/plugins/gltf.js
CHANGED
|
@@ -405,6 +405,11 @@ async function parseGLTF(data, baseUrl='')
|
|
|
405
405
|
for (const name of json.extensionsRequired || [])
|
|
406
406
|
if (name === 'KHR_draco_mesh_compression' || name === 'EXT_meshopt_compression')
|
|
407
407
|
throw new Error(`glTF with ${name} is not read, export it uncompressed`);
|
|
408
|
+
else if (name === 'KHR_texture_basisu')
|
|
409
|
+
throw new Error('glTF with KHR_texture_basisu textures is not read, export them as png or jpeg');
|
|
410
|
+
// one that only may use them has other images to fall back on, or none: said, since it loads with them left out
|
|
411
|
+
if (json.extensionsUsed?.includes('KHR_texture_basisu') && !json.extensionsRequired?.includes('KHR_texture_basisu'))
|
|
412
|
+
console.warn('glTF: KHR_texture_basisu textures are not read and are left out, export them as png or jpeg');
|
|
408
413
|
|
|
409
414
|
// the buffers: the GLB's own, a data uri, or a file beside the model
|
|
410
415
|
const buffers = await Promise.all((json.buffers || []).map((buffer, i)=>
|
|
@@ -447,7 +452,8 @@ async function parseGLTF(data, baseUrl='')
|
|
|
447
452
|
const bitmap = normalTextures.has(index) ?
|
|
448
453
|
await createImageBitmap(blob, {colorSpaceConversion: 'none', premultiplyAlpha: 'none'}) :
|
|
449
454
|
opaque ? await createImageBitmap(blob, {premultiplyAlpha: 'none'}).then(gltfOpaqueImage) : await createImageBitmap(blob);
|
|
450
|
-
|
|
455
|
+
// REPEAT by default, and hard edged only when its sampler says NEAREST, not as the game's tiles are
|
|
456
|
+
return new TextureInfo(bitmap, true, [sampler.wrapS ?? 10497, sampler.wrapT ?? 10497], sampler.magFilter === 9728);
|
|
451
457
|
}
|
|
452
458
|
catch (e) { LOG('glTF image not loaded', e); }
|
|
453
459
|
}));
|
|
@@ -557,17 +563,25 @@ function gltfOpaqueImage(image)
|
|
|
557
563
|
{
|
|
558
564
|
const gl = glContext, {width, height} = image;
|
|
559
565
|
if (gl.isContextLost()) return image; // nothing to read back through, it keeps its alpha
|
|
560
|
-
|
|
561
|
-
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
562
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
563
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
564
|
-
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
566
|
+
// the room to read into first: for a huge image it can fail, and must not with the framebuffer bound
|
|
565
567
|
const data = new Uint8ClampedArray(width * height * 4);
|
|
566
|
-
gl.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
const texture = gl.createTexture(), framebuffer = gl.createFramebuffer(), bound = gl.getParameter(gl.FRAMEBUFFER_BINDING);
|
|
569
|
+
try
|
|
570
|
+
{
|
|
571
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
572
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
|
573
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
574
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
575
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); // row 0 is the image's first row, as uploaded
|
|
576
|
+
}
|
|
577
|
+
finally
|
|
578
|
+
{
|
|
579
|
+
// the 2D renderer's own bindings back, whatever happened
|
|
580
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, bound);
|
|
581
|
+
gl.bindTexture(gl.TEXTURE_2D, glActiveTexture);
|
|
582
|
+
gl.deleteFramebuffer(framebuffer);
|
|
583
|
+
gl.deleteTexture(texture);
|
|
584
|
+
}
|
|
571
585
|
for (let i = 3; i < data.length; i += 4)
|
|
572
586
|
data[i] = 255;
|
|
573
587
|
image.close();
|
|
@@ -704,7 +718,7 @@ function gltfAccessor(json, buffers, index)
|
|
|
704
718
|
}
|
|
705
719
|
if (scale !== 1)
|
|
706
720
|
for (let i = 0; i < out.length; ++i)
|
|
707
|
-
out[i]
|
|
721
|
+
out[i] = max(out[i] / scale, -1); // a signed type's lowest value is -1 too, as the format says
|
|
708
722
|
return {data: out, components, count: a.count};
|
|
709
723
|
}
|
|
710
724
|
|
|
@@ -768,8 +782,10 @@ function gltfPart(json, buffers, textures, primitive, matrix, name)
|
|
|
768
782
|
const [er, eg, eb] = material.emissiveFactor || [0, 0, 0];
|
|
769
783
|
if (er || eg || eb)
|
|
770
784
|
{
|
|
785
|
+
// KHR_materials_emissive_strength makes it that many times brighter, emission above 1 in Blender
|
|
786
|
+
const strength = material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
|
|
771
787
|
part.emissiveMap = emissiveRef ? textures[emissiveRef.index] : gltfWhiteTexture();
|
|
772
|
-
part.emissiveMapColor = rgb(gltfSRGB(er), gltfSRGB(eg), gltfSRGB(eb));
|
|
788
|
+
part.emissiveMapColor = rgb(gltfSRGB(er) * strength, gltfSRGB(eg) * strength, gltfSRGB(eb) * strength);
|
|
773
789
|
}
|
|
774
790
|
return part;
|
|
775
791
|
}
|
package/plugins/postProcess.js
CHANGED
|
@@ -158,8 +158,11 @@ class PostProcessPlugin
|
|
|
158
158
|
{
|
|
159
159
|
// copy main canvas to work canvas at the backing store size,
|
|
160
160
|
// mainCanvasSize is css pixels so it would lose resolution
|
|
161
|
-
|
|
162
|
-
workCanvas.height
|
|
161
|
+
// sized again only when the canvas changed, setting a size remakes the canvas even to the same one
|
|
162
|
+
if (workCanvas.width !== mainCanvas.width || workCanvas.height !== mainCanvas.height)
|
|
163
|
+
workCanvas.width = mainCanvas.width, workCanvas.height = mainCanvas.height;
|
|
164
|
+
else
|
|
165
|
+
workContext.clearRect(0, 0, workCanvas.width, workCanvas.height);
|
|
163
166
|
glCopyToContext(workContext);
|
|
164
167
|
workContext.drawImage(mainCanvas, 0, 0);
|
|
165
168
|
// clear the main canvas with clearRect, resizing it would also
|
package/plugins/render3d.js
CHANGED
|
@@ -302,17 +302,19 @@ function render3DInstance(mesh, matrix, tileInfo, color)
|
|
|
302
302
|
// make room for one more instance of a mesh under a texture and the current draw state, flushing a batch that
|
|
303
303
|
// differs first, and return where its 24 floats go in mesh.instanceData: the matrix, the tint and the uv rect
|
|
304
304
|
// drawMesh passes whether its matrix mirrors, and its batch then culls by the mesh and winds by that at the flush,
|
|
305
|
-
// so it never sets the draw state for them, which would move the state's version at every draw
|
|
306
|
-
|
|
305
|
+
// so it never sets the draw state for them, which would move the state's version at every draw; drawBillboard
|
|
306
|
+
// passes that its batch is unlit, for the same reason
|
|
307
|
+
function render3DInstanceSlot(mesh, textureInfo, mirrored, unlit=false)
|
|
307
308
|
{
|
|
308
309
|
const r = render3D;
|
|
309
310
|
if (mesh.instanceCount && (mesh.instanceTextureInfo !== textureInfo || mesh.instanceMirrored !== mirrored
|
|
310
|
-
|| render3DStateChanged(mesh.instanceState)))
|
|
311
|
+
|| mesh.instanceUnlit !== unlit || render3DStateChanged(mesh.instanceState)))
|
|
311
312
|
render3DFlushInstances(mesh);
|
|
312
313
|
if (!mesh.instanceCount)
|
|
313
314
|
{
|
|
314
315
|
mesh.instanceTextureInfo = textureInfo;
|
|
315
316
|
mesh.instanceMirrored = mirrored;
|
|
317
|
+
mesh.instanceUnlit = unlit;
|
|
316
318
|
mesh.instanceState = render3DCaptureBatchState();
|
|
317
319
|
r.instanceMeshes.push(mesh);
|
|
318
320
|
}
|
|
@@ -346,6 +348,8 @@ function render3DFlushInstances(only)
|
|
|
346
348
|
const state = mesh.instanceState;
|
|
347
349
|
if (mesh.instanceMirrored !== undefined) // a drawMesh batch: culled by its mesh, wound by its matrices
|
|
348
350
|
state.cullBackFaces = !mesh.doubleSided, state.mirrored = mesh.instanceMirrored;
|
|
351
|
+
if (mesh.instanceUnlit) // a batch of sprites
|
|
352
|
+
state.lighting = false;
|
|
349
353
|
render3DDrawInstanced(mesh, buffer, count, mesh.instanceTextureInfo, state);
|
|
350
354
|
}
|
|
351
355
|
if (!only)
|
|
@@ -702,6 +706,7 @@ class Render3DPlugin
|
|
|
702
706
|
/** @type {TextureInfo|undefined} */
|
|
703
707
|
this.streamTileInfo = undefined;
|
|
704
708
|
this.streamState = undefined; // captured state the pending batch was drawn under
|
|
709
|
+
this.streamUnlit = false; // the pending batch is drawn unlit whatever that state says, billboards are
|
|
705
710
|
/** @type {Mesh|undefined} */
|
|
706
711
|
this.capture = undefined; // the mesh a bake is filling
|
|
707
712
|
/** @type {Array<{distance: number, state: Object, draw: function(): void}>|undefined} */
|
|
@@ -1092,6 +1097,8 @@ class Render3DPlugin
|
|
|
1092
1097
|
{
|
|
1093
1098
|
if (!this.streamCount || !render3DCanDraw()) return;
|
|
1094
1099
|
const gl = glContext;
|
|
1100
|
+
if (this.streamUnlit)
|
|
1101
|
+
this.streamState.lighting = false; // its own copy of the state
|
|
1095
1102
|
render3DSetDrawUniforms(RENDER3D_IDENTITY, this.streamTileInfo, WHITE, RENDER3D_FULL_UV_RECT, this.streamState);
|
|
1096
1103
|
render3DBindVertexBuffer(this.streamBuffer);
|
|
1097
1104
|
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.streamFloats, 0, this.streamCount * RENDER3D_VERTEX_FLOATS);
|
|
@@ -1326,13 +1333,34 @@ class Render3DPlugin
|
|
|
1326
1333
|
return this.queueTransparent(p, ()=> this.drawBillboard(p, s, tileInfo, c, angle, upright));
|
|
1327
1334
|
}
|
|
1328
1335
|
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
const
|
|
1332
|
-
this.
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
+
// unlit on screen, in the shadow map the object's flag decides; the batch is told, the draw state is not
|
|
1337
|
+
// set, which would move its version at every sprite
|
|
1338
|
+
const unlit = !this.shadowPass;
|
|
1339
|
+
if (!this.blend && this.depthTest && this.instancing)
|
|
1340
|
+
{
|
|
1341
|
+
// an opaque sprite is one more instance of the shared quad, as a mesh's uses are: every sprite of a
|
|
1342
|
+
// sheet is one instanced draw at the end of the stage, with nothing written per corner; the shader
|
|
1343
|
+
// drops its see through texels, so it needs no sorting
|
|
1344
|
+
if (!render3DCanDraw() || this.shadowPass && !this.lighting) return; // unlit things cast no shadow
|
|
1345
|
+
if (this.frustumCulling && !render3DSphereVisible(pos.x, pos.y, pos.z, hypot(size.x, size.y) / 2))
|
|
1346
|
+
return;
|
|
1347
|
+
const quad = this.billboardMesh, cb = this.cameraBack, uv = render3DGetTileUVs(tileInfo);
|
|
1348
|
+
render3DMeshUpload(quad);
|
|
1349
|
+
// the quad's half axes, doubled for its unit square
|
|
1350
|
+
const a = render3DBillboardAxes(size, angle, upright);
|
|
1351
|
+
const k = render3DInstanceSlot(quad, render3DTextureOf(tileInfo), false, unlit), data = quad.instanceData;
|
|
1352
|
+
data[k] = a[0] * 2; data[k+1] = a[1] * 2; data[k+2] = a[2] * 2; data[k+3] = 0;
|
|
1353
|
+
data[k+4] = a[3] * 2; data[k+5] = a[4] * 2; data[k+6] = a[5] * 2; data[k+7] = 0;
|
|
1354
|
+
data[k+8] = cb.x; data[k+9] = cb.y; data[k+10] = cb.z; data[k+11] = 0;
|
|
1355
|
+
data[k+12] = pos.x; data[k+13] = pos.y; data[k+14] = pos.z; data[k+15] = 1;
|
|
1356
|
+
data[k+16] = color.r; data[k+17] = color.g; data[k+18] = color.b; data[k+19] = color.a;
|
|
1357
|
+
data[k+20] = uv.x; data[k+21] = uv.y; data[k+22] = uv.w; data[k+23] = uv.h;
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// one that blends is drawn in its sorted place: the quad's six stream vertices written straight in with no
|
|
1362
|
+
// vectors made, for sprites that blend, and for particles when instancing is off
|
|
1363
|
+
const uvRect = render3DBeginStrip(6, tileInfo, unlit);
|
|
1336
1364
|
if (!uvRect) return;
|
|
1337
1365
|
const a = render3DBillboardAxes(size, angle, upright);
|
|
1338
1366
|
const rx = a[0], ry = a[1], rz = a[2], ux = a[3], uy = a[4], uz = a[5];
|
|
@@ -2444,8 +2472,9 @@ function render3DRenderDepth()
|
|
|
2444
2472
|
// which way they face, so keeping the count even keeps every strip facing out.
|
|
2445
2473
|
|
|
2446
2474
|
// make room in the stream for a strip of count vertices under the current state and texture, flushing a batch that
|
|
2447
|
-
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn
|
|
2448
|
-
|
|
2475
|
+
// differs first; returns the uv rect to map the vertices with, or undefined when nothing can be drawn; unlit draws
|
|
2476
|
+
// the batch unlit whatever the state says, so a billboard does not set the state and set it back at every draw
|
|
2477
|
+
function render3DBeginStrip(count, tileInfo, unlit=false)
|
|
2449
2478
|
{
|
|
2450
2479
|
const r = render3D;
|
|
2451
2480
|
if (!render3DCanDraw()) return;
|
|
@@ -2453,12 +2482,13 @@ function render3DBeginStrip(count, tileInfo)
|
|
|
2453
2482
|
ASSERT(count <= RENDER3D_MAX_STREAM_VERTS, 'strip is too large for the stream, bake it into a mesh');
|
|
2454
2483
|
if (count > RENDER3D_MAX_STREAM_VERTS) return;
|
|
2455
2484
|
const textureInfo = render3DTextureOf(tileInfo);
|
|
2456
|
-
if (r.streamCount && (textureInfo !== r.streamTileInfo ||
|
|
2457
|
-
|| r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
2485
|
+
if (r.streamCount && (textureInfo !== r.streamTileInfo || r.streamUnlit !== unlit
|
|
2486
|
+
|| render3DStateChanged(r.streamState) || r.streamCount + count > RENDER3D_MAX_STREAM_VERTS))
|
|
2458
2487
|
r.flush();
|
|
2459
2488
|
render3DFlushBeforeOverlay();
|
|
2460
2489
|
if (!r.streamCount)
|
|
2461
2490
|
r.streamState = render3DCaptureBatchState();
|
|
2491
|
+
r.streamUnlit = unlit;
|
|
2462
2492
|
r.streamTileInfo = textureInfo;
|
|
2463
2493
|
return render3DGetTileUVs(tileInfo);
|
|
2464
2494
|
}
|
|
@@ -528,7 +528,7 @@ function editor3DLevelLoaded(level)
|
|
|
528
528
|
const original = editor3DCopy(editor3DObjects()), originalParts = editor3DLevelParts();
|
|
529
529
|
const hash = editor3DContentHash(original, originalParts);
|
|
530
530
|
// sceneBase is the scene the game set, taken before the level's own block is applied
|
|
531
|
-
const record = {fileName: url
|
|
531
|
+
const record = {fileName: editorFileName(url, 'level3D.json'), key: url ?? 'level #' + hash, original,
|
|
532
532
|
originalParts, sceneBase: render3D ? editor3DSceneState() : undefined, hash, pending: undefined,
|
|
533
533
|
fileHandle: undefined, undo: [], redo: []};
|
|
534
534
|
editor3DRecords.set(level, record);
|
|
@@ -629,6 +629,7 @@ function editor3DChange(change)
|
|
|
629
629
|
const before = editor3DCopy(editor3DObjects()), after = editor3DCopy(before);
|
|
630
630
|
change(after);
|
|
631
631
|
if (editor3DSame(before, after)) return false;
|
|
632
|
+
editor3DFixIds(after); // an object added with no id, or with one in use, gets one of its own
|
|
632
633
|
editor3DSetObjects(after);
|
|
633
634
|
editor3DStroke ||= {before, parts: editor3DLevelParts()};
|
|
634
635
|
return true;
|
|
@@ -1402,7 +1403,7 @@ function editor3DPrefabJSON(name)
|
|
|
1402
1403
|
|
|
1403
1404
|
// the prefabs opened one inside the other, each with the level it was opened from, its name, the selection there
|
|
1404
1405
|
// and the ids of that level's objects that had something made for them
|
|
1405
|
-
/** @type {Array<{level: Object, name: string, selection: Array<number>, made: Set<number>, entered?: string
|
|
1406
|
+
/** @type {Array<{level: Object, name: string, selection: Array<number>, made: Set<number>, entered?: string}>} */
|
|
1406
1407
|
const editor3DPrefabStack = [];
|
|
1407
1408
|
// the levels that are a prefab being edited, they have no autosave of their own
|
|
1408
1409
|
const editor3DPrefabLevels = new WeakSet;
|
|
@@ -1411,6 +1412,28 @@ const editor3DPrefabLevels = new WeakSet;
|
|
|
1411
1412
|
let editor3DPrefabEdits = {};
|
|
1412
1413
|
// the prefabs of the game's own that were edited and are not in a file yet
|
|
1413
1414
|
const editor3DPrefabDirty = new Set;
|
|
1415
|
+
// what is known of each prefab's file, by its name, so it outlasts the prefab being closed and opened again: the
|
|
1416
|
+
// save being written, for the next to wait on, what the file has since a Save, and what the prefab holds now
|
|
1417
|
+
/** @type {Map<string, {saving?: Promise<any>, saved?: string, content?: string}>} */
|
|
1418
|
+
const editor3DPrefabFiles = new Map;
|
|
1419
|
+
function editor3DPrefabFile(name)
|
|
1420
|
+
{
|
|
1421
|
+
editor3DPrefabFiles.has(name) || editor3DPrefabFiles.set(name, {});
|
|
1422
|
+
return editor3DPrefabFiles.get(name);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// say if a prefab of the game's own is in its file, now that it holds this: once a Save has written it, it is
|
|
1426
|
+
// when what it holds is what was written; before any Save it is not once it was edited
|
|
1427
|
+
function editor3DPrefabFileCheck(name, content, edited=false)
|
|
1428
|
+
{
|
|
1429
|
+
const file = editor3DPrefabFile(name);
|
|
1430
|
+
file.content = content;
|
|
1431
|
+
if (level3DPrefabs.get(name)?.fromLevel) return; // one of the level's own is saved with the level
|
|
1432
|
+
if (file.saved !== undefined)
|
|
1433
|
+
content === file.saved ? editor3DPrefabDirty.delete(name) : editor3DPrefabDirty.add(name);
|
|
1434
|
+
else if (edited)
|
|
1435
|
+
editor3DPrefabDirty.add(name);
|
|
1436
|
+
}
|
|
1414
1437
|
|
|
1415
1438
|
// destroy what the level being edited has in the world, its objects' and its maps'
|
|
1416
1439
|
function editor3DPrefabClear()
|
|
@@ -1464,9 +1487,10 @@ function editor3DPrefabBack()
|
|
|
1464
1487
|
level3DPrefabSet(frame.name, prefab, fromLevel);
|
|
1465
1488
|
if (fromLevel)
|
|
1466
1489
|
editor3DPrefabEdits[frame.name] = prefab;
|
|
1467
|
-
else if (JSON.stringify(objects) !== frame.saved)
|
|
1468
|
-
editor3DPrefabDirty.add(frame.name); // not what Save wrote while it was open
|
|
1469
1490
|
}
|
|
1491
|
+
// one of the game's own is in its file or not by what it holds now, edited here or not: undone back to how it
|
|
1492
|
+
// was opened after a Save, it is not what the file has
|
|
1493
|
+
editor3DPrefabFileCheck(frame.name, JSON.stringify(objects), changed);
|
|
1470
1494
|
|
|
1471
1495
|
// the level it was opened from is the editor's again, with its own undo
|
|
1472
1496
|
const level = editor3DLevel = frame.level, record = editor3DRecords.get(level);
|
|
@@ -1794,8 +1818,10 @@ function editor3DAutosave(level=editor3DLevel, known)
|
|
|
1794
1818
|
if (known?.fromLevel && JSON.stringify(objects) !== frame.entered)
|
|
1795
1819
|
edits[frame.name] = known.attached ? {attached: true, objects} : {objects};
|
|
1796
1820
|
});
|
|
1797
|
-
|
|
1798
|
-
|
|
1821
|
+
// with nothing edited any more, an undo back to how the prefab was, the level's autosave is the level's own
|
|
1822
|
+
// again, which takes away one of an edit that was undone and keeps the level's other edits
|
|
1823
|
+
const prefabs = {...editor3DLevelPart('prefabs', root), ...edits};
|
|
1824
|
+
root && editor3DAutosave(Object.keys(prefabs).length ? {...root, prefabs} : root, editor3DRecords.get(root));
|
|
1799
1825
|
return;
|
|
1800
1826
|
}
|
|
1801
1827
|
const record = known ?? editor3DRecords.get(level);
|
|
@@ -1809,12 +1835,9 @@ function editor3DAutosave(level=editor3DLevel, known)
|
|
|
1809
1835
|
else
|
|
1810
1836
|
saves[record.key] = {hash: record.hash, savedHash: record.savedHash, objects: editor3DCopy(objects),
|
|
1811
1837
|
...parts};
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
editor3DSaveFailed = false;
|
|
1816
|
-
}
|
|
1817
|
-
catch { editor3DSaveFailed = true; }
|
|
1838
|
+
const failed = editorSaveFailed; // the 2D editor's own flag is its own
|
|
1839
|
+
editor3DSaveFailed = editorWriteSaves(saves, record.key, editor3DSaveName());
|
|
1840
|
+
editorSaveFailed = failed;
|
|
1818
1841
|
}
|
|
1819
1842
|
|
|
1820
1843
|
// put the autosaved edits of a file that changed into the level, as one undo
|
|
@@ -1871,16 +1894,23 @@ async function editor3DSave(pickAgain=false)
|
|
|
1871
1894
|
// inside a prefab, Save writes the prefab as a file of its own, or the game keeps it itself
|
|
1872
1895
|
const text = editor3DLevelJSON(level), name = open.name + '.json';
|
|
1873
1896
|
const written = JSON.stringify(editor3DObjects()); // as the file has it
|
|
1897
|
+
// its place in line is the prefab's, by its name: closed and opened again it is a new level here, and its
|
|
1898
|
+
// saves still go in the order they were asked for
|
|
1899
|
+
const file = editor3DPrefabFile(open.name), before = file.saving;
|
|
1874
1900
|
const saved = (async ()=>
|
|
1875
1901
|
{
|
|
1876
|
-
await
|
|
1902
|
+
await before;
|
|
1877
1903
|
const kept = await editorCall('onSave', text, name) === true;
|
|
1878
1904
|
kept || saveText(text, name, 'application/json');
|
|
1879
|
-
|
|
1880
|
-
|
|
1905
|
+
file.saved = written;
|
|
1906
|
+
// it is in its file when what it holds now is what was written: edited again while the save was
|
|
1907
|
+
// kept, or gone back from with more edits, it still waits
|
|
1908
|
+
const at = editor3DPrefabStack.indexOf(open);
|
|
1909
|
+
const now = at < 0 ? file.content : JSON.stringify((editor3DPrefabStack[at + 1]?.level ?? editor3DLevel).objects);
|
|
1910
|
+
editor3DPrefabFileCheck(open.name, now ?? written);
|
|
1881
1911
|
return kept ? 'kept' : 'downloaded';
|
|
1882
1912
|
})();
|
|
1883
|
-
|
|
1913
|
+
file.saving = saved.catch(()=> {});
|
|
1884
1914
|
return saved;
|
|
1885
1915
|
}
|
|
1886
1916
|
// what is written, kept as it is now: the level can change while the file is picked and written, and those
|
package/plugins/render3dLevel.js
CHANGED
|
@@ -116,7 +116,8 @@ let level3DVoxelTiles, level3DVoxelSetupMap, level3DVoxelMap;
|
|
|
116
116
|
|
|
117
117
|
/** How a level's block map is made: the sheet its blocks show tiles of, and a function to set it up
|
|
118
118
|
* - A level's voxels block makes a VoxelMap when the level loads, with texture 0 and the default tile size unless
|
|
119
|
-
* a sheet is given here; a block's type shows that tile of the sheet on every face
|
|
119
|
+
* a sheet is given here; a block's type shows that tile of the sheet on every face; a game that has loaded no
|
|
120
|
+
* image gets plain blocks, a color for each type
|
|
120
121
|
* - setup is called with each map a level makes, to give block types their own faces or make them see-through
|
|
121
122
|
* - Call it before level3DLoad; with no arguments the defaults are back
|
|
122
123
|
* @param {TileInfo} [tileInfo] - The sheet's first tile, as for a VoxelMap
|
|
@@ -178,7 +179,7 @@ function level3DVoxelsMake(voxels)
|
|
|
178
179
|
{
|
|
179
180
|
const shape = level3DVoxelsShape(voxels);
|
|
180
181
|
if (!shape || typeof VoxelMap == 'undefined') return;
|
|
181
|
-
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles ||
|
|
182
|
+
const map = new VoxelMap(shape.pos, shape.size, level3DVoxelTiles || level3DVoxelPlainTiles());
|
|
182
183
|
level3DVoxelsDecode(voxels.blocks, map.data);
|
|
183
184
|
level3DVoxelSetupMap?.(map);
|
|
184
185
|
map.rebuild();
|
|
@@ -240,6 +241,31 @@ function level3DTerrainSetColors(map, terrain, paint=level3DTerrainPaint(terrain
|
|
|
240
241
|
map.colors = map.heights.map((row, r)=> row.map((v, c)=> palette[paint.cells[r * map.columns + c]] || base));
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
// the sheet a level's blocks show when the game gave none with level3DVoxelSetup: the game's first image, as a
|
|
245
|
+
// VoxelMap takes it, or, for a game that has loaded no image, plain tiles made here, a color for each type, so a
|
|
246
|
+
// block map in a game with no art can still be seen and painted
|
|
247
|
+
let level3DVoxelPlain;
|
|
248
|
+
function level3DVoxelPlainTiles()
|
|
249
|
+
{
|
|
250
|
+
if (textureInfos[0]?.size.x || headlessMode || typeof OffscreenCanvas == 'undefined')
|
|
251
|
+
return tile();
|
|
252
|
+
if (!level3DVoxelPlain)
|
|
253
|
+
{
|
|
254
|
+
// 16 by 16 tiles of 16 pixels, each a color of its own with a darker edge, so the blocks read as blocks
|
|
255
|
+
const context = createCanvasContext(256);
|
|
256
|
+
for (let i = 0; i < 256; ++i)
|
|
257
|
+
{
|
|
258
|
+
const x = i % 16 * 16, y = (i / 16 | 0) * 16, hue = i * .618 % 1;
|
|
259
|
+
context.fillStyle = hsl(hue, .45, .4).toString();
|
|
260
|
+
context.fillRect(x, y, 16, 16);
|
|
261
|
+
context.fillStyle = hsl(hue, .5, .55).toString();
|
|
262
|
+
context.fillRect(x + 1, y + 1, 14, 14);
|
|
263
|
+
}
|
|
264
|
+
level3DVoxelPlain = new TextureInfo(context.canvas);
|
|
265
|
+
}
|
|
266
|
+
return tile(0, 16, level3DVoxelPlain, 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
243
269
|
// the HeightMap of a level's terrain block, undefined when it has none or it is wrong
|
|
244
270
|
function level3DTerrainMake(terrain)
|
|
245
271
|
{
|
package/plugins/textureSheet.js
CHANGED
|
@@ -303,6 +303,7 @@ function loadAtlas(imageSrc, jsonSrc, padding=textureSheetPadding)
|
|
|
303
303
|
for (const group of parseAtlas(data))
|
|
304
304
|
{
|
|
305
305
|
// reserve a block of full size cells, one per frame
|
|
306
|
+
if (!group.frames.length) continue; // a tag with no frames, the groups after it still load
|
|
306
307
|
const sourceSize = group.frames[0].sourceSize;
|
|
307
308
|
const blockSize = vec2(sourceSize.x*group.frames.length, sourceSize.y);
|
|
308
309
|
const added = textureSheetAdd(blockSize, sourceSize, padding);
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -593,45 +593,50 @@ function tweenUpdate(gameDelta, realDelta)
|
|
|
593
593
|
const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
|
|
594
594
|
for (const t of tweenActive)
|
|
595
595
|
list.push(t);
|
|
596
|
-
|
|
596
|
+
// the list is let go however the walk ends: a callback that throws must not leave it held, or every
|
|
597
|
+
// update after would take it for an update still going and make a list of its own
|
|
598
|
+
try
|
|
597
599
|
{
|
|
598
|
-
|
|
599
|
-
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
600
|
-
// counts on from this one
|
|
601
|
-
if (!t.active || t.activePass >= pass) continue;
|
|
602
|
-
let dt;
|
|
603
|
-
if (enginePath)
|
|
600
|
+
for (let i = list.length; i--;)
|
|
604
601
|
{
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
t.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
602
|
+
const t = list[i];
|
|
603
|
+
// stopped, or started again by a callback this update, or during an update a callback ran inside it, which
|
|
604
|
+
// counts on from this one
|
|
605
|
+
if (!t.active || t.activePass >= pass) continue;
|
|
606
|
+
let dt;
|
|
607
|
+
if (enginePath)
|
|
608
|
+
{
|
|
609
|
+
// a paused tween keeps count too, so it does not jump when resumed
|
|
610
|
+
dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
|
|
611
|
+
t.lastTime = time;
|
|
612
|
+
t.lastTimeReal = timeReal;
|
|
613
|
+
}
|
|
614
|
+
else
|
|
615
|
+
dt = t.useRealTime ? realDelta : gameDelta;
|
|
616
|
+
if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
|
|
617
|
+
if (t.paused || dt <= 0) continue;
|
|
614
618
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
619
|
+
t.life -= dt;
|
|
620
|
+
if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
|
|
621
|
+
{
|
|
622
|
+
t.callback(t.interp(t.life));
|
|
623
|
+
}
|
|
624
|
+
else
|
|
625
|
+
{
|
|
626
|
+
// Completion: fire end value, remove from active, start the next iteration
|
|
627
|
+
// of a loop or pingPong, or when there is none it has completed, fire onComplete
|
|
628
|
+
t.callback(t.interp(0));
|
|
629
|
+
if (!t.active || t.activePass >= pass)
|
|
630
|
+
continue; // stopped or restarted by its own callback, the run it was on ends without completing
|
|
631
|
+
tweenDeactivate(t);
|
|
632
|
+
const next = t.thenCallback;
|
|
633
|
+
t.thenCallback = undefined;
|
|
634
|
+
if (!(next && next()) && t.onComplete)
|
|
635
|
+
t.onComplete();
|
|
636
|
+
}
|
|
632
637
|
}
|
|
633
638
|
}
|
|
634
|
-
list.length = 0;
|
|
639
|
+
finally { list.length = 0; }
|
|
635
640
|
}
|
|
636
641
|
|
|
637
642
|
/** Stop every active tween, ending loops too, without calling their then-callbacks.
|
package/plugins/uiSystem.js
CHANGED
|
@@ -615,8 +615,7 @@ class UISystemPlugin
|
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
/** Destroy and remove all objects
|
|
619
|
-
* @memberof UISystem */
|
|
618
|
+
/** Destroy and remove all objects */
|
|
620
619
|
destroyObjects()
|
|
621
620
|
{
|
|
622
621
|
for (const o of this.uiObjects)
|
|
@@ -1137,8 +1136,10 @@ class UIObject
|
|
|
1137
1136
|
return void inputClearKey(0, 0, false, true, false);
|
|
1138
1137
|
}
|
|
1139
1138
|
}
|
|
1139
|
+
// the object that was the active one going into this update: one pressed and let go inside a
|
|
1140
|
+
// frame is clicked on the next, with its release, not on both
|
|
1140
1141
|
if (!uiSystem.activateOnPress)
|
|
1141
|
-
if (!mouseDown && this.isActiveObject() && this.interactive)
|
|
1142
|
+
if (!mouseDown && isActive && this.isActiveObject() && this.interactive)
|
|
1142
1143
|
this.click();
|
|
1143
1144
|
if (this.destroyed) return;
|
|
1144
1145
|
}
|
|
@@ -1881,7 +1882,10 @@ class UIVideo extends UIObject
|
|
|
1881
1882
|
if (this.destroyed)
|
|
1882
1883
|
return;
|
|
1883
1884
|
|
|
1885
|
+
// let go of the media too, a paused video keeps what it has loaded
|
|
1884
1886
|
this.video.pause();
|
|
1887
|
+
this.video.removeAttribute?.('src');
|
|
1888
|
+
this.video.load?.();
|
|
1885
1889
|
this.video.remove();
|
|
1886
1890
|
super.destroy();
|
|
1887
1891
|
}
|