@bitmagic/cli 0.1.52-dev.7 → 0.1.52-dev.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/levels.d.ts +33 -0
- package/dist/commands/levels.js +241 -0
- package/dist/commands/levels.js.map +1 -0
- package/dist/editor/journal.d.ts +13 -0
- package/dist/editor/journal.js +2 -0
- package/dist/editor/journal.js.map +1 -1
- package/dist/editor/server.js +72 -91
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +119 -114
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/editor/voxel-save.js +8 -12
- package/dist/editor/voxel-save.js.map +1 -1
- package/dist/levels/registry.d.ts +88 -0
- package/dist/levels/registry.js +309 -0
- package/dist/levels/registry.js.map +1 -0
- package/dist/scaffold/project-files.js +3 -1
- package/dist/scaffold/project-files.js.map +1 -1
- package/package.json +4 -4
|
@@ -370,6 +370,24 @@ ${ASSET_DETAIL_HTML}
|
|
|
370
370
|
if (frame.contentWindow) frame.contentWindow.postMessage(message, '*');
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
/**
|
|
374
|
+
* POST JSON to the sidecar and read its JSON answer.
|
|
375
|
+
*
|
|
376
|
+
* Every endpoint replies { ok, ... }, and an HTTP failure means the same thing to this page as an
|
|
377
|
+
* ok:false body, so the two are folded into one flag rather than re-checked at every call site.
|
|
378
|
+
* Rejects only when the request never landed or the answer was not JSON, which the callers that
|
|
379
|
+
* can say something useful about it catch.
|
|
380
|
+
*/
|
|
381
|
+
async function postJson(path, body) {
|
|
382
|
+
var response = await fetch(path, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify(body)
|
|
386
|
+
});
|
|
387
|
+
var result = await response.json();
|
|
388
|
+
return { ok: response.ok && result.ok === true, result: result };
|
|
389
|
+
}
|
|
390
|
+
|
|
373
391
|
/**
|
|
374
392
|
* What this host can do, for the editor's feature detection (game/src/editor/EditorHost.ts).
|
|
375
393
|
*
|
|
@@ -391,27 +409,21 @@ ${ASSET_DETAIL_HTML}
|
|
|
391
409
|
});
|
|
392
410
|
}
|
|
393
411
|
|
|
394
|
-
/** Write the URL the engine just uploaded terrain to into src/work/world.json. */
|
|
395
412
|
/**
|
|
396
413
|
* Persist an object voxel sculpt. Mirrors recordTerrainSave: the bytes are already in storage,
|
|
397
414
|
* only the record crosses this boundary.
|
|
398
415
|
*/
|
|
399
416
|
async function recordVoxelSave(message) {
|
|
400
417
|
try {
|
|
401
|
-
var
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
body: JSON.stringify({
|
|
405
|
-
asset: message.asset,
|
|
406
|
-
environmentObjects: message.environmentObjects || []
|
|
407
|
-
})
|
|
418
|
+
var saved = await postJson('/api/editor/voxel-saved', {
|
|
419
|
+
asset: message.asset,
|
|
420
|
+
environmentObjects: message.environmentObjects || []
|
|
408
421
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
setStatus('error', result.error || 'Voxel edit could not be saved');
|
|
422
|
+
if (!saved.ok) {
|
|
423
|
+
setStatus('error', saved.result.error || 'Voxel edit could not be saved');
|
|
412
424
|
return;
|
|
413
425
|
}
|
|
414
|
-
lastKnownMtime = result.worldMtimeMs || lastKnownMtime;
|
|
426
|
+
lastKnownMtime = saved.result.worldMtimeMs || lastKnownMtime;
|
|
415
427
|
lastLocalEditAt = Date.now();
|
|
416
428
|
setStatus('saved', 'Voxel edit saved to world.json');
|
|
417
429
|
} catch (error) {
|
|
@@ -419,17 +431,13 @@ ${ASSET_DETAIL_HTML}
|
|
|
419
431
|
}
|
|
420
432
|
}
|
|
421
433
|
|
|
434
|
+
/** Write the URL the engine just uploaded terrain to into src/work/world.json. */
|
|
422
435
|
async function recordTerrainSave(voxelUrl) {
|
|
423
436
|
if (!voxelUrl) return;
|
|
424
437
|
try {
|
|
425
|
-
var
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
body: JSON.stringify({ voxelUrl: voxelUrl })
|
|
429
|
-
});
|
|
430
|
-
var result = await response.json();
|
|
431
|
-
if (!response.ok || result.ok !== true) throw new Error(result.error || 'save failed');
|
|
432
|
-
lastKnownMtime = result.worldMtimeMs || lastKnownMtime;
|
|
438
|
+
var saved = await postJson('/api/editor/terrain-saved', { voxelUrl: voxelUrl });
|
|
439
|
+
if (!saved.ok) throw new Error(saved.result.error || 'save failed');
|
|
440
|
+
lastKnownMtime = saved.result.worldMtimeMs || lastKnownMtime;
|
|
433
441
|
setStatus('saved', 'Terrain saved to world.json');
|
|
434
442
|
} catch (error) {
|
|
435
443
|
// The bytes ARE uploaded — only the one-line reference failed to land. Say so precisely, so
|
|
@@ -508,11 +516,8 @@ ${ASSET_DETAIL_HTML}
|
|
|
508
516
|
case 'EDITOR_JOURNAL_EVENT':
|
|
509
517
|
// Terrain and voxel saves, which the journal's world.json diff cannot see — and their
|
|
510
518
|
// failures, which change no file at all.
|
|
511
|
-
void
|
|
512
|
-
|
|
513
|
-
headers: { 'Content-Type': 'application/json' },
|
|
514
|
-
body: JSON.stringify(message)
|
|
515
|
-
}).catch(function () { /* the journal is a convenience; never break the editor for it */ });
|
|
519
|
+
void postJson('/api/editor/journal', message)
|
|
520
|
+
.catch(function () { /* the journal is a convenience; never break the editor for it */ });
|
|
516
521
|
break;
|
|
517
522
|
|
|
518
523
|
case 'VOXEL_ASSET_SAVED':
|
|
@@ -623,13 +628,11 @@ ${ASSET_DETAIL_HTML}
|
|
|
623
628
|
if (!hqAsset) return;
|
|
624
629
|
var prompt = hqPrompt.value.trim();
|
|
625
630
|
if (prompt === '') { hqPrompt.focus(); return; }
|
|
626
|
-
var
|
|
627
|
-
|
|
628
|
-
headers: { 'Content-Type': 'application/json' },
|
|
629
|
-
body: JSON.stringify({ assetId: hqAsset.id, assetName: hqAsset.name, prompt: prompt })
|
|
631
|
+
var saved = await postJson('/api/editor/hq-request', {
|
|
632
|
+
assetId: hqAsset.id, assetName: hqAsset.name, prompt: prompt
|
|
630
633
|
});
|
|
631
|
-
var result =
|
|
632
|
-
if (!
|
|
634
|
+
var result = saved.result;
|
|
635
|
+
if (!saved.ok) {
|
|
633
636
|
setStatus('error', result.error || 'Could not record the request');
|
|
634
637
|
closeHqDialog();
|
|
635
638
|
return;
|
|
@@ -911,13 +914,11 @@ ${ASSET_DETAIL_HTML}
|
|
|
911
914
|
saving = true;
|
|
912
915
|
setStatus('saving', 'Saving…');
|
|
913
916
|
try {
|
|
914
|
-
var
|
|
915
|
-
|
|
916
|
-
headers: { 'Content-Type': 'application/json' },
|
|
917
|
-
body: JSON.stringify({ changes: changes, status: status, markersDirty: markersDirty })
|
|
917
|
+
var saved = await postJson('/api/scene/save', {
|
|
918
|
+
changes: changes, status: status, markersDirty: markersDirty
|
|
918
919
|
});
|
|
919
|
-
var result =
|
|
920
|
-
if (!
|
|
920
|
+
var result = saved.result;
|
|
921
|
+
if (!saved.ok) {
|
|
921
922
|
setStatus('error', result.error || 'Save failed');
|
|
922
923
|
return;
|
|
923
924
|
}
|
|
@@ -934,7 +935,6 @@ ${ASSET_DETAIL_HTML}
|
|
|
934
935
|
}
|
|
935
936
|
}
|
|
936
937
|
|
|
937
|
-
|
|
938
938
|
/**
|
|
939
939
|
* The pre-SSE way of noticing an agent's edit, kept as the fallback for a dropped stream.
|
|
940
940
|
*
|
|
@@ -1022,12 +1022,21 @@ ${ASSET_DETAIL_HTML}
|
|
|
1022
1022
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1025
|
+
/**
|
|
1026
|
+
* A span of time in the panel's vocabulary: seconds under a minute, then minutes and seconds.
|
|
1027
|
+
* One formatter for a step's duration and a job's elapsed time alike, so two rows of the same
|
|
1028
|
+
* forge cannot count in different shapes. '' for anything that is not a real duration.
|
|
1029
|
+
*/
|
|
1030
|
+
function formatDuration(ms) {
|
|
1031
|
+
if (typeof ms !== 'number' || !isFinite(ms) || ms < 0) return '';
|
|
1032
|
+
var seconds = Math.round(ms / 1000);
|
|
1029
1033
|
if (seconds < 60) return seconds + 's';
|
|
1030
|
-
return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's';
|
|
1034
|
+
return Math.floor(seconds / 60) + 'm ' + String(seconds % 60).padStart(2, '0') + 's';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/** How long ago an ISO timestamp was. */
|
|
1038
|
+
function formatElapsed(since) {
|
|
1039
|
+
return formatDuration(Date.now() - Date.parse(since));
|
|
1031
1040
|
}
|
|
1032
1041
|
|
|
1033
1042
|
/** When an asset was made, or 0 for the ones that came with the template. */
|
|
@@ -1054,6 +1063,13 @@ ${ASSET_DETAIL_HTML}
|
|
|
1054
1063
|
return dated.concat(undated);
|
|
1055
1064
|
}
|
|
1056
1065
|
|
|
1066
|
+
/** A membership lookup over a list of asset ids — what '/api/assets' answers with its previews. */
|
|
1067
|
+
function idSet(ids) {
|
|
1068
|
+
var set = {};
|
|
1069
|
+
for (var i = 0; i < ids.length; i++) set[ids[i]] = true;
|
|
1070
|
+
return set;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1057
1073
|
function el(tag, className, text) {
|
|
1058
1074
|
var node = document.createElement(tag);
|
|
1059
1075
|
if (className) node.className = className;
|
|
@@ -1063,14 +1079,6 @@ ${ASSET_DETAIL_HTML}
|
|
|
1063
1079
|
return node;
|
|
1064
1080
|
}
|
|
1065
1081
|
|
|
1066
|
-
/** A step duration, in the same vocabulary as formatElapsed but from milliseconds. */
|
|
1067
|
-
function formatDuration(ms) {
|
|
1068
|
-
if (typeof ms !== 'number' || !isFinite(ms) || ms < 0) return '';
|
|
1069
|
-
var seconds = Math.round(ms / 1000);
|
|
1070
|
-
if (seconds < 60) return seconds + 's';
|
|
1071
|
-
return Math.floor(seconds / 60) + 'm ' + String(seconds % 60).padStart(2, '0') + 's';
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
1082
|
function formatCount(value) {
|
|
1075
1083
|
return typeof value === 'number' && isFinite(value) ? value.toLocaleString() : '';
|
|
1076
1084
|
}
|
|
@@ -1223,6 +1231,28 @@ ${ASSET_DETAIL_HTML}
|
|
|
1223
1231
|
return row;
|
|
1224
1232
|
}
|
|
1225
1233
|
|
|
1234
|
+
var MAX_VXL_PREVIEW_BYTES = 8 * 1024 * 1024;
|
|
1235
|
+
|
|
1236
|
+
/** The mesh route: the asset itself, or the GLB a voxelized one was baked from. */
|
|
1237
|
+
function glbJobFor(asset) {
|
|
1238
|
+
var glb = asset.sourceGlbUrl || asset.url;
|
|
1239
|
+
// Double-escaped because this whole page is a template literal: a single backslash is consumed
|
|
1240
|
+
// by the literal, and the browser would receive the invalid regex /.(glb|gltf)(?|$)/.
|
|
1241
|
+
if (typeof glb !== 'string' || !/\\.(glb|gltf)(\\?|$)/i.test(glb)) return null;
|
|
1242
|
+
return { kind: 'glb', url: glb };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** The voxel route, for assets small enough to be worth shipping through a postMessage. */
|
|
1246
|
+
function vxlJobFor(asset) {
|
|
1247
|
+
var url = asset.url;
|
|
1248
|
+
if (typeof url !== 'string' || !/\\.vxl(\\?|$)/i.test(url)) return null;
|
|
1249
|
+
// The voxel message carries the FILE, so this one costs a fetch and a structured clone of the
|
|
1250
|
+
// whole thing. The templates' files are 0.5-2.5 KB; a multi-megabyte one is a hitch on the
|
|
1251
|
+
// render thread in exchange for a 46px tile, and the glyph is the better trade.
|
|
1252
|
+
if (typeof asset.size === 'number' && asset.size > MAX_VXL_PREVIEW_BYTES) return null;
|
|
1253
|
+
return { kind: 'vxl', url: url };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1226
1256
|
/**
|
|
1227
1257
|
* How the engine should draw this asset's thumbnail, or null when there is nothing to draw.
|
|
1228
1258
|
*
|
|
@@ -1240,20 +1270,8 @@ ${ASSET_DETAIL_HTML}
|
|
|
1240
1270
|
* An asset with neither — a sound, an animation, a level — keeps its glyph, which is the honest
|
|
1241
1271
|
* answer for something with no appearance.
|
|
1242
1272
|
*/
|
|
1243
|
-
var MAX_VXL_PREVIEW_BYTES = 8 * 1024 * 1024;
|
|
1244
|
-
|
|
1245
1273
|
function previewJobFor(asset) {
|
|
1246
|
-
|
|
1247
|
-
// Double-escaped because this whole page is a template literal: a single backslash is consumed
|
|
1248
|
-
// by the literal, and the browser would receive the invalid regex /.(glb|gltf)(?|$)/.
|
|
1249
|
-
if (typeof glb === 'string' && /\\.(glb|gltf)(\\?|$)/i.test(glb)) return { kind: 'glb', url: glb };
|
|
1250
|
-
var url = asset.url;
|
|
1251
|
-
if (typeof url !== 'string' || !/\\.vxl(\\?|$)/i.test(url)) return null;
|
|
1252
|
-
// The voxel message carries the FILE, so this one costs a fetch and a structured clone of the
|
|
1253
|
-
// whole thing. The templates' files are 0.5-2.5 KB; a multi-megabyte one is a hitch on the
|
|
1254
|
-
// render thread in exchange for a 46px tile, and the glyph is the better trade.
|
|
1255
|
-
if (typeof asset.size === 'number' && asset.size > MAX_VXL_PREVIEW_BYTES) return null;
|
|
1256
|
-
return { kind: 'vxl', url: url };
|
|
1274
|
+
return glbJobFor(asset) || vxlJobFor(asset);
|
|
1257
1275
|
}
|
|
1258
1276
|
|
|
1259
1277
|
function assetRow(asset, isFresh, hasPreview) {
|
|
@@ -1317,30 +1335,26 @@ ${ASSET_DETAIL_HTML}
|
|
|
1317
1335
|
|
|
1318
1336
|
function renderAssets(assets, jobs, previews) {
|
|
1319
1337
|
var sorted = sortAssets(assets);
|
|
1320
|
-
var cached =
|
|
1321
|
-
for (var p = 0; p < previews.length; p++) cached[previews[p]] = true;
|
|
1338
|
+
var cached = idSet(previews);
|
|
1322
1339
|
|
|
1323
1340
|
assetsJobs.textContent = '';
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
var ids = {};
|
|
1327
|
-
|
|
1328
|
-
for
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
signature.push(sorted[i].id + (cached[sorted[i].id] ? '*' : ''));
|
|
1333
|
-
}
|
|
1334
|
-
var joined = signature.join(',');
|
|
1341
|
+
jobs.forEach(function (job) { assetsJobs.appendChild(jobRow(job)); });
|
|
1342
|
+
|
|
1343
|
+
var ids = idSet(sorted.map(function (asset) { return asset.id; }));
|
|
1344
|
+
// The preview state is part of the signature: a thumbnail landing is a change worth redrawing
|
|
1345
|
+
// for, and it is the only one that alters a row without altering the asset set.
|
|
1346
|
+
var signature = sorted.map(function (asset) {
|
|
1347
|
+
return asset.id + (cached[asset.id] ? '*' : '');
|
|
1348
|
+
}).join(',');
|
|
1335
1349
|
|
|
1336
|
-
if (
|
|
1350
|
+
if (signature !== lastAssetSignature) {
|
|
1337
1351
|
assetsList.textContent = '';
|
|
1338
|
-
|
|
1339
|
-
var fresh = seenAssetIds !== null && !seenAssetIds[
|
|
1340
|
-
assetsList.appendChild(assetRow(
|
|
1341
|
-
}
|
|
1352
|
+
sorted.forEach(function (asset) {
|
|
1353
|
+
var fresh = seenAssetIds !== null && !seenAssetIds[asset.id];
|
|
1354
|
+
assetsList.appendChild(assetRow(asset, fresh, cached[asset.id] === true));
|
|
1355
|
+
});
|
|
1342
1356
|
seenAssetIds = ids;
|
|
1343
|
-
lastAssetSignature =
|
|
1357
|
+
lastAssetSignature = signature;
|
|
1344
1358
|
}
|
|
1345
1359
|
|
|
1346
1360
|
lastJobs = jobs;
|
|
@@ -1446,12 +1460,10 @@ ${ASSET_DETAIL_HTML}
|
|
|
1446
1460
|
continue;
|
|
1447
1461
|
}
|
|
1448
1462
|
|
|
1449
|
-
var saved = await (
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
})).json();
|
|
1454
|
-
if (saved.stored === true) stored += 1;
|
|
1463
|
+
var saved = await postJson('/api/assets/preview', {
|
|
1464
|
+
assetId: asset.id, image: response.previewBase64
|
|
1465
|
+
});
|
|
1466
|
+
if (saved.result.stored === true) stored += 1;
|
|
1455
1467
|
}
|
|
1456
1468
|
} catch (error) {
|
|
1457
1469
|
// A thumbnail is a decoration; losing the queue must not take the panel with it.
|
|
@@ -1510,8 +1522,8 @@ ${ASSET_DETAIL_HTML}
|
|
|
1510
1522
|
var assetReturnFocus = null;
|
|
1511
1523
|
var assetLoadTimer = null;
|
|
1512
1524
|
|
|
1513
|
-
var IMAGE_TYPES = { image:
|
|
1514
|
-
var AUDIO_TYPES = { audio:
|
|
1525
|
+
var IMAGE_TYPES = { image: true, skybox: true };
|
|
1526
|
+
var AUDIO_TYPES = { audio: true, sound: true, 'sound-effect': true };
|
|
1515
1527
|
|
|
1516
1528
|
/**
|
|
1517
1529
|
* What the VIEWER should load — which is not always what the thumbnail drew.
|
|
@@ -1521,14 +1533,7 @@ ${ASSET_DETAIL_HTML}
|
|
|
1521
1533
|
* shows the asset: a .vxl is drawn as voxels even when the mesh it was baked from is on record.
|
|
1522
1534
|
*/
|
|
1523
1535
|
function detailJobFor(asset) {
|
|
1524
|
-
|
|
1525
|
-
if (typeof url === 'string' && /\\.vxl(\\?|$)/i.test(url)) {
|
|
1526
|
-
if (typeof asset.size === 'number' && asset.size > MAX_VXL_PREVIEW_BYTES) return null;
|
|
1527
|
-
return { kind: 'vxl', url: url };
|
|
1528
|
-
}
|
|
1529
|
-
var glb = asset.sourceGlbUrl || url;
|
|
1530
|
-
if (typeof glb === 'string' && /\\.(glb|gltf)(\\?|$)/i.test(glb)) return { kind: 'glb', url: glb };
|
|
1531
|
-
return null;
|
|
1536
|
+
return vxlJobFor(asset) || glbJobFor(asset);
|
|
1532
1537
|
}
|
|
1533
1538
|
|
|
1534
1539
|
function fact(term, value) {
|
|
@@ -1563,22 +1568,24 @@ ${ASSET_DETAIL_HTML}
|
|
|
1563
1568
|
fact('Voxels', typeof asset.voxelCount === 'number' ? String(asset.voxelCount) : '');
|
|
1564
1569
|
fact('Id', asset.id || '');
|
|
1565
1570
|
|
|
1566
|
-
assetNote.
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
assetAudio.classList.add('hidden');
|
|
1570
|
-
assetResetButton.classList.add('hidden');
|
|
1571
|
+
[assetNote, assetFrame, assetImage, assetAudio, assetResetButton].forEach(function (node) {
|
|
1572
|
+
node.classList.add('hidden');
|
|
1573
|
+
});
|
|
1571
1574
|
if (assetLoadTimer) clearTimeout(assetLoadTimer);
|
|
1572
1575
|
|
|
1573
1576
|
var type = String(asset.type || '').toLowerCase();
|
|
1574
1577
|
var job = detailJobFor(asset);
|
|
1578
|
+
// The asset's own file, for the types a browser can show without a renderer.
|
|
1579
|
+
var direct = typeof asset.url === 'string' ? asset.url : '';
|
|
1575
1580
|
|
|
1576
|
-
|
|
1581
|
+
// '=== true' rather than a truthy read: a type of 'constructor' would otherwise find
|
|
1582
|
+
// Object.prototype's own property and pass for an image.
|
|
1583
|
+
if (IMAGE_TYPES[type] === true && direct) {
|
|
1577
1584
|
// An image is already the thing. Loading a 3D viewer to show a picture would be theatre.
|
|
1578
|
-
assetImage.src =
|
|
1585
|
+
assetImage.src = direct;
|
|
1579
1586
|
assetImage.classList.remove('hidden');
|
|
1580
|
-
} else if (AUDIO_TYPES[type] ===
|
|
1581
|
-
assetAudio.src =
|
|
1587
|
+
} else if (AUDIO_TYPES[type] === true && direct) {
|
|
1588
|
+
assetAudio.src = direct;
|
|
1582
1589
|
assetAudio.classList.remove('hidden');
|
|
1583
1590
|
} else if (job !== null) {
|
|
1584
1591
|
assetResetButton.classList.remove('hidden');
|
|
@@ -1695,14 +1702,12 @@ ${ASSET_DETAIL_HTML}
|
|
|
1695
1702
|
* into the creator's file.
|
|
1696
1703
|
*/
|
|
1697
1704
|
function withThumbnails(assets, previews) {
|
|
1698
|
-
var cached =
|
|
1699
|
-
for (var i = 0; i < previews.length; i++) cached[previews[i]] = true;
|
|
1705
|
+
var cached = idSet(previews);
|
|
1700
1706
|
return assets.map(function (asset) {
|
|
1701
1707
|
if (asset.screenshotUrl || !cached[asset.id]) return asset;
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
return copy;
|
|
1708
|
+
return Object.assign({}, asset, {
|
|
1709
|
+
screenshotUrl: SHELL_ORIGIN + '/api/assets/preview/' + encodeURIComponent(asset.id) + '.png'
|
|
1710
|
+
});
|
|
1706
1711
|
});
|
|
1707
1712
|
}
|
|
1708
1713
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EACL,qBAAqB,EAAE,kBAAkB,GAC1C,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+CAA+C,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE7F,oGAAoG;AACpG,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;;;;;;;GAYG;AACH,SAAS,sBAAsB;IAC7B,OAAO;QACL,6BAA6B,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,GAAG;QACjE,iCAAiC,qBAAqB,CAAC,QAAQ,EAAE,GAAG;QACpE,8BAA8B,kBAAkB,CAAC,QAAQ,EAAE,GAAG;KAC/D,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,kGAAkG;AAClG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AASD,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,OAAO,GAAG,oBAAoB,OAAO,CAAC,QAAQ,2BAA2B,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;IACpH,OAAO;;;;wBAIe,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;EAChD,kBAAkB;;EAElB,mBAAmB;EACnB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsHrB,gBAAgB;EAChB,eAAe;;;;;;uBAMM,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8B/C,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;EAyBhB,iBAAiB;;;;mBAIA,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;kBACxB,gBAAgB;yBACT,qBAAqB;2BACnB,gBAAgB;;IAEvC,sBAAsB,EAAE
|
|
1
|
+
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAomD3B,CAAC;AACF,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isJsonObject } from '../project/world-json.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/** Match an array item by its `id`, the predicate every write below uses. Mirrors `save.ts`. */
|
|
3
|
+
function byId(id) {
|
|
4
|
+
return (item) => isJsonObject(item) && item.id === id;
|
|
4
5
|
}
|
|
5
6
|
/**
|
|
6
7
|
* Returns null when there is nothing safe to write.
|
|
@@ -14,32 +15,27 @@ export function buildVoxelSaveModifications(payload, world) {
|
|
|
14
15
|
if (!isJsonObject(asset) || typeof asset.id !== 'string' || asset.id === '')
|
|
15
16
|
return null;
|
|
16
17
|
const assetId = asset.id;
|
|
17
|
-
const
|
|
18
|
+
const isThisAsset = byId(assetId);
|
|
19
|
+
const existing = (Array.isArray(world.assets) ? world.assets : []).some(isThisAsset);
|
|
18
20
|
const modifications = [existing
|
|
19
21
|
? {
|
|
20
22
|
type: 'updateRoot',
|
|
21
23
|
path: ['assets'],
|
|
22
|
-
predicate:
|
|
24
|
+
predicate: isThisAsset,
|
|
23
25
|
// Field by field, so anything the agent added since LOAD_GAME survives. See the header.
|
|
24
26
|
value: (item) => ({ ...(isJsonObject(item) ? item : {}), ...asset }),
|
|
25
27
|
}
|
|
26
|
-
: {
|
|
27
|
-
type: 'upsertRoot',
|
|
28
|
-
path: ['assets'],
|
|
29
|
-
predicate: (item) => isJsonObject(item) && item.id === assetId,
|
|
30
|
-
value: asset,
|
|
31
|
-
}];
|
|
28
|
+
: { type: 'upsertRoot', path: ['assets'], predicate: isThisAsset, value: asset }];
|
|
32
29
|
// Instances the save minted. Upserted by id rather than pushed, so a retry of the same save does
|
|
33
30
|
// not leave two copies of one object standing in the same place.
|
|
34
31
|
const instances = Array.isArray(payload.environmentObjects) ? payload.environmentObjects : [];
|
|
35
32
|
for (const instance of instances) {
|
|
36
33
|
if (!isJsonObject(instance) || typeof instance.id !== 'string' || instance.id === '')
|
|
37
34
|
continue;
|
|
38
|
-
const instanceId = instance.id;
|
|
39
35
|
modifications.push({
|
|
40
36
|
type: 'upsertRoot',
|
|
41
37
|
path: ['environmentObjects'],
|
|
42
|
-
predicate: (
|
|
38
|
+
predicate: byId(instance.id),
|
|
43
39
|
value: instance,
|
|
44
40
|
});
|
|
45
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voxel-save.js","sourceRoot":"","sources":["../../src/editor/voxel-save.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,YAAY,EAAmB,MAAM,0BAA0B,CAAC;AAkBzE,SAAS,
|
|
1
|
+
{"version":3,"file":"voxel-save.js","sourceRoot":"","sources":["../../src/editor/voxel-save.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,YAAY,EAAmB,MAAM,0BAA0B,CAAC;AAkBzE,gGAAgG;AAChG,SAAS,IAAI,CAAC,EAAU;IACtB,OAAO,CAAC,IAAa,EAAW,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CACzC,OAAyB,EACzB,KAAiB;IAEjB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACzF,MAAM,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;IAEzB,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACrF,MAAM,aAAa,GAA4B,CAAC,QAAQ;YACtD,CAAC,CAAC;gBACA,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,CAAC,QAAQ,CAAC;gBAChB,SAAS,EAAE,WAAW;gBACtB,wFAAwF;gBACxF,KAAK,EAAE,CAAC,IAAa,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC;aAC9E;YACD,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAEpF,iGAAiG;IACjG,iEAAiE;IACjE,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9F,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,OAAO,QAAQ,CAAC,EAAE,KAAK,QAAQ,IAAI,QAAQ,CAAC,EAAE,KAAK,EAAE;YAAE,SAAS;QAC/F,aAAa,CAAC,IAAI,CAAC;YACjB,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,CAAC,oBAAoB,CAAC;YAC5B,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,aAAa;QACb,OAAO;QACP,SAAS,EAAE,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;QACrF,MAAM,EAAE,QAAQ;KACjB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bitmagic levels` — the level registry in `worldProfileData.levels`, without the web Creator.
|
|
3
|
+
*
|
|
4
|
+
* `forge` can CREATE levels but never could manage them: only a game's first forge becomes the
|
|
5
|
+
* start level (or `forge --make-start` at creation), so fixing a wrong start, renaming, or deleting
|
|
6
|
+
* a level meant opening the Creator.
|
|
7
|
+
*
|
|
8
|
+
* ── The invariant this file exists to respect ────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* `worldProfileData` keeps three LEGACY MIRRORS — `voxelUrl`, `spawnPoints` and
|
|
11
|
+
* `playerSpawnPosition` — describing the START level, because frozen genre and work-template code
|
|
12
|
+
* boots from those rather than from the registry (see `WorldLevel` in game/src/types/game.ts).
|
|
13
|
+
*
|
|
14
|
+
* Nothing self-heals on the normal boot path. `applyBootLevelOverride` does recompute them, but
|
|
15
|
+
* only when the main screen's level chooser passes a `bootLevelId`. So a `startLevelId` moved
|
|
16
|
+
* without its mirrors boots the NEW level's objects, lighting and navmesh over the OLD level's
|
|
17
|
+
* terrain, and nothing throws. That is the failure this module exists to prevent.
|
|
18
|
+
*
|
|
19
|
+
* The mirrors come from `buildStartLevelMirrors` in `@bitmagic/world-forger`, deliberately rather
|
|
20
|
+
* than reimplemented — the same function the forge uses, carrying a subtlety worth not
|
|
21
|
+
* rediscovering: spawn mirrors are written ONLY when the level owns spawn points, because a level
|
|
22
|
+
* without its own inherits the globals, which are then already correct.
|
|
23
|
+
*
|
|
24
|
+
* Pure: it computes modifications and never applies them.
|
|
25
|
+
*/
|
|
26
|
+
import { type LevelEntry, type WorldJsonModification } from '@bitmagic/world-forger/pipeline/index.js';
|
|
27
|
+
import { type JsonObject } from '../project/world-json.js';
|
|
28
|
+
export interface LevelRow {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
vwldAssetId: string;
|
|
32
|
+
/** The `.vwld` asset is absent from `assets[]`, so this level has nothing to load. */
|
|
33
|
+
missingAsset: boolean;
|
|
34
|
+
start: boolean;
|
|
35
|
+
spawnPoints: number;
|
|
36
|
+
/** Placed objects tagged to this level. Untagged ones are global and belong to none. */
|
|
37
|
+
placed: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function listLevels(world: JsonObject): LevelRow[];
|
|
40
|
+
export interface LevelPlan {
|
|
41
|
+
modifications: WorldJsonModification[];
|
|
42
|
+
/** Said after the write. Never a reason to refuse. */
|
|
43
|
+
notes: string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Make `levelId` the level the game boots into — registry AND mirrors, together.
|
|
47
|
+
*
|
|
48
|
+
* Refused when the level's `.vwld` asset is missing or has no url: the mirrors are what actually
|
|
49
|
+
* boot the game, so writing `startLevelId` alone would leave `voxelUrl` describing the previous
|
|
50
|
+
* level, and the registry and the thing that loads would disagree with no error anywhere.
|
|
51
|
+
*/
|
|
52
|
+
export declare function planSetStart(world: JsonObject, levelId: string): LevelPlan;
|
|
53
|
+
export declare function planRename(world: JsonObject, levelId: string, name: string): LevelPlan;
|
|
54
|
+
export interface AddPlan extends LevelPlan {
|
|
55
|
+
level: LevelEntry;
|
|
56
|
+
/** True when this made the game multi-level for the first time. */
|
|
57
|
+
converted: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Register an existing `.vwld` asset as a level.
|
|
61
|
+
*
|
|
62
|
+
* Start-level policy matches `forge`'s: a game's FIRST level becomes the start, later ones do not,
|
|
63
|
+
* and `--make-start` overrides. Stealing the start from a working game because someone registered
|
|
64
|
+
* a second level would be the surprising outcome.
|
|
65
|
+
*/
|
|
66
|
+
export declare function planAdd(world: JsonObject, vwldAssetId: string, options: {
|
|
67
|
+
name?: string;
|
|
68
|
+
makeStart: boolean;
|
|
69
|
+
}): AddPlan;
|
|
70
|
+
export interface RemovePlan extends LevelPlan {
|
|
71
|
+
level: LevelEntry;
|
|
72
|
+
/** Placed instances tagged to this level, which go with it. */
|
|
73
|
+
placedRemoved: number;
|
|
74
|
+
/** Doors and key items scoped to it, which also go. */
|
|
75
|
+
scopedRemoved: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Remove a level from the registry.
|
|
79
|
+
*
|
|
80
|
+
* Two refusals, matching the agent's `manage-levels delete` so the two lanes cannot disagree:
|
|
81
|
+
* the last level, and the START level. The second matters most — `startLevelId` naming nothing
|
|
82
|
+
* falls back to `levels[0]` at boot while the mirrors still describe the deleted level, which is
|
|
83
|
+
* the silent stale-mirror failure. Move the start first, deliberately.
|
|
84
|
+
*
|
|
85
|
+
* The `.vwld` asset is left alone: it may be large, nothing here deletes uploaded bytes, and a
|
|
86
|
+
* re-add is then free.
|
|
87
|
+
*/
|
|
88
|
+
export declare function planRemove(world: JsonObject, levelId: string): RemovePlan;
|