Progress bar and steps while generating, loading, opening claims and travelling; saving chip (t30)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 18:54:06 -04:00
co-authored by Claude Opus 5.5
parent 4d5fca6dae
commit 66543bbd4b
13 changed files with 162 additions and 34 deletions
+8 -4
View File
@@ -82,11 +82,13 @@ async function openNewClaim() {
carried.money -= 5000;
let seed; do { seed = Math.floor(Math.random() * 900000) + 100000; } while (seed === worldSeed);
const name = claimName(seed, (await listWorlds()).map(w => w.name));
await setupNewWorld(seed, w, h, name, carried);
await setupNewWorld(seed, w, h, name, carried, 'Opening a new claim');
debt = CLAIM_DEBT * 2 ** (n - 1);
career = { ...career, claims: [...career.claims, GAME.worldId], claimNo: n };
GAME.claimNo = n;
showLoading(true, 0.9, 'Saving');
if (!await saveWorld('auto', true)) throw new Error('Save failed');
showLoading(true, 1, 'Saving');
showLoading(false); closeAllModals(); enterGame();
showCard({ kind: 'New claim', title: name, body: [`Claim ${n}. You owe ${fmtMoney(debt)} on this claim. Your money, bag, upgrades and what you’ve learned came along. Buildings stayed on the old claim.`], hold: 1.8 });
return true;
@@ -105,12 +107,14 @@ async function travel(worldId) {
try {
const entry = (await listWorlds()).find(w => w.id === worldId);
if (!entry || entry.career !== GAME.careerId) return false;
const progress = (f, s) => showLoading(true, f, s, 'Travelling to ' + entry.name);
progress(0, 'Saving');
if (!await saveWorld('main', true)) throw new Error('Save failed');
const sv = await worldNewest(worldId);
if (!sv || !await startFromSave(sv)) return false;
const sv = await worldNewest(worldId, progress);
if (!sv || !await startFromSave(sv, progress)) return false;
closeAllModals(); arrived = true; return true;
} catch (e) { toast('Could not travel. Check browser storage and try again.'); return false; }
finally { claimBusy = false; if (!arrived) GAME.paused = paused; }
finally { showLoading(false); claimBusy = false; if (!arrived) GAME.paused = paused; }
}
async function renderClaimsTab(body) {
body.innerHTML = '<p class=note>Loading claims…</p>';
+3 -2
View File
@@ -16,6 +16,7 @@ function frame(now) {
const dtReal = Math.min(0.25, (now - lastT) / 1000); lastT = now;
if (SET.fpsCap && now - lastRender < 1000 / SET.fpsCap - 1) return;
const dt = Math.min(0.1, (now - (lastRender || now)) / 1000) || 1 / 60; lastRender = now;
if (GAME.loading) { acc = 0; return; }
if (!GAME.running) { pollPad(); return; }
const t0 = performance.now();
const pad = pollPad();
@@ -110,7 +111,7 @@ function boot(hotData) {
const [w, h] = $('nw-size').value.split('x').map(Number);
startNewWorld(seed, w, h, $('nw-name').value);
};
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); const sv = await newestSave(); if (sv) await startFromSave(sv); else toast('No saved world found.'); };
$('btn-continue').onclick = async () => { auInit(); playSfx('ui'); await loadSavedWorld(); };
$('btn-title-settings').onclick = () => { auInit(); playSfx('ui'); openSettings('Display'); };
$('btn-title-help').onclick = () => { auInit(); playSfx('ui'); openNotebook('Controls'); };
const touchOnly = matchMedia('(pointer: coarse)').matches && !matchMedia('(any-pointer: fine)').matches;
@@ -118,7 +119,7 @@ function boot(hotData) {
$('title').hidden = true;
migrateWorlds().then(() => refreshContinue()).then(async () => {
$('title').hidden = false; if (!TITLE.raf) drawTitle();
if (hotData && hotData.resume) { const sv = await newestSave(); if (sv) await startFromSave(sv); }
if (hotData && hotData.resume) { await loadSavedWorld(); }
}).catch(() => { $('title').hidden = false; if (!TITLE.raf) drawTitle(); });
requestAnimationFrame(frame);
}
+19 -7
View File
@@ -40,12 +40,13 @@ function putWorldEntry(entry) {
await DB.put('worlds', list);
});
}
async function worldNewest(id) {
async function worldNewest(id, onProgress) {
if (onProgress) onProgress(0.05, 'Reading the save');
const a = await DB.get(`${id}:main`), b = await DB.get(`${id}:auto`);
const sv = a && b ? (a.savedAt >= b.savedAt ? a : b) : a || b;
if (!sv) return null;
const entry = (await listWorlds()).find(w => w.id === id);
return { ...sv, buffers: await unpackBuffers(sv.buffers), worldId: id, worldName: entry ? entry.name : 'World' };
return { ...sv, buffers: await unpackBuffers(sv.buffers, onProgress), worldId: id, worldName: entry ? entry.name : 'World' };
}
function deleteWorld(id) {
return worldWrite(async () => {
@@ -98,7 +99,10 @@ async function migrateWorlds() {
});
} catch (e) { toast('Could not move the old save. It has been kept. Reload to try again.'); throw e; }
}
let savingCount = 0;
async function saveWorld(slot, quiet) {
savingCount++;
const indicator = setTimeout(() => { if (!GAME.loading) $('saving').hidden = false; }, 150);
try {
const s = serializeWorld(), savedAt = Date.now(), playTime = GAME.playTime;
if (!GAME.worldId) { GAME.worldId = newWorldId(); GAME.worldName = `World ${(await listWorlds()).length + 1}`; }
@@ -116,10 +120,10 @@ async function saveWorld(slot, quiet) {
} catch (e) {
if (!quiet) toast('Could not save. Browser storage is unavailable in this view. Use Export world file instead.');
return false;
}
} finally { clearTimeout(indicator); if (!--savingCount) $('saving').hidden = true; }
}
async function newestSave() {
try { for (const entry of await listWorlds()) { const sv = await worldNewest(entry.id); if (sv) return sv; } return null; }
async function newestSave(onProgress) {
try { for (const entry of await listWorlds()) { const sv = await worldNewest(entry.id, onProgress); if (sv) return sv; } return null; }
catch (e) { return null; }
}
function b64FromBytes(u8) { let s = ''; const CH = 0x8000; for (let i = 0; i < u8.length; i += CH) s += String.fromCharCode.apply(null, u8.subarray(i, i + CH)); return btoa(s); }
@@ -137,9 +141,11 @@ async function packBuffers(buffers) {
}
return packed;
}
async function unpackBuffers(buffers) {
async function unpackBuffers(buffers, onProgress) {
let n = 0; const total = Object.keys(buffers).length;
const raw = {}, types = { Uint8Array, Uint8ClampedArray, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array };
for (const k in buffers) {
if (onProgress) onProgress(0.1 + 0.6 * n++ / total, 'Unpacking');
const b = buffers[k];
if (ArrayBuffer.isView(b)) { raw[k] = b; continue; }
if (!b || b.gz !== true) throw new Error('Invalid saved buffer: ' + k);
@@ -148,6 +154,7 @@ async function unpackBuffers(buffers) {
if (!bytes) throw new Error('This browser cannot decompress saved worlds.');
raw[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
}
if (onProgress) onProgress(0.7, 'Unpacking');
return raw;
}
async function exportWorld() {
@@ -177,22 +184,27 @@ async function exportWorld() {
catch (e) { if (e && e.code !== 'declined') toast('The world file was not saved.'); }
}
async function importWorldFile(file) {
const progress = (f, s) => showLoading(true, f, s, 'Loading world');
progress(0.05, 'Reading the save');
try {
const obj = JSON.parse(await file.text());
if (obj.format !== 'deepgrain-world') throw new Error('Not a Deepgrain world file.');
const bufs = {};
const T = { Uint8Array, Uint16Array, Int16Array, Float32Array, Int32Array };
let n = 0; const total = Object.keys(obj.buffers).length;
for (const k in obj.buffers) {
progress(0.1 + 0.6 * n++ / total, 'Unpacking');
const b = obj.buffers[k]; let bytes = bytesFromB64(b.b64);
if (b.gz) bytes = await gz(bytes, true);
const C = T[b.type] || Uint8Array;
bufs[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
}
const id = newWorldId(), name = `Imported ${obj.meta.seed}`.slice(0, 40);
if (!await startFromSave({ meta: obj.meta, buffers: bufs, worldId: id, worldName: name })) return;
if (!await startFromSave({ meta: obj.meta, buffers: bufs, worldId: id, worldName: name }, progress)) return;
if (!await saveWorld('auto')) return;
toast('World imported.');
} catch (e) { toast('That file could not be loaded: ' + (e.message || 'unknown error')); }
finally { showLoading(false); }
}
// ---------- Blueprint library ----------
+30 -6
View File
@@ -1,6 +1,12 @@
// Starting, loading and quitting a game; modal dialogs; the pause menu.
// ---------- Start / stop ----------
function showLoading(on, p) {
function showLoading(on, p, label, title) {
GAME.loading = on;
if (on) {
$('saving').hidden = true;
if (title) $('loading').querySelector('.label').textContent = title;
$('loadstep').textContent = label || '';
}
$('loading').hidden = !on;
if (on) { $('loadbar').style.width = Math.round((p || 0) * 100) + '%'; $('loadtext').textContent = Math.round((p || 0) * 100) + '%'; }
}
@@ -15,11 +21,11 @@ function enterGame() {
GAME.drag = null; GAME.lmb = GAME.rmb = false; lastBagKey = '';
hudDirty = true; renderHotbar();
}
async function setupNewWorld(seed, w, h, name, carried) {
auInit(); showLoading(true, 0);
async function setupNewWorld(seed, w, h, name, carried, title = 'Generating world') {
auInit(); showLoading(true, 0, 'Shaping the land', title);
await new Promise(resolve => setTimeout(resolve, 30));
for (const k of Object.keys(stats)) stats[k] = 0;
generateWorld(seed, w, h, null);
await generateWorldAsync(seed, w, h, (f, s) => showLoading(true, f * 0.9, s, title));
GAME.worldId = newWorldId(); GAME.worldName = name;
GAME.playTime = 0; GAME.clip = null; GAME.paste = false; GAME.selDrag = null; GAME.tool = 0; GAME.blow.clear(); GAME.wallMat = RSTONE; GAME.filterCfg = { m: [], inv: 0 }; GAME.pieceIdx = 0;
if (carried) applyCareerState(carried);
@@ -30,30 +36,48 @@ async function startNewWorld(seed, w, h, name = '') {
await setupNewWorld(seed, w, h, cleanWorldName(name, `World ${worlds.length + 1}`));
career = { id: newWorldId(), claims: [GAME.worldId], claimNo: 1 };
GAME.careerId = career.id; GAME.claimNo = 1;
showLoading(true, 0.9, 'Saving');
await saveWorld('auto');
showLoading(true, 1, 'Saving');
showLoading(false); closeAllModals(); enterGame();
showCard({ kind: 'Welcome', title: 'The dig site', body: ['Fly with WASD. The Drill (1) breaks rock loose. The Pneumatic nozzle (2) picks up and blows the selection. Click or Q/E in the bag selects; Ctrl+click mixes. Hold Ctrl to see everything found. Clear returns to picking what you click. Shift: everything.', 'You owe $20,000 on this claim. Sell what you dig and make at the trade dock next to you: anything that drops through its grate is paid for on the spot, and buyers post orders that pay far more. Every tip lives in the notebook (N).'], hold: 1.8 });
} catch (e) { showLoading(false); toast('Could not create a world. Browser storage is unavailable.'); }
}
async function startFromSave(sv) {
async function startFromSave(sv, onProgress) {
onProgress = onProgress || ((f, s) => showLoading(true, f, s, 'Loading world'));
auInit();
GAME.paused = true;
try {
for (const k of Object.keys(stats)) stats[k] = 0;
onProgress(0.75, 'Building the world');
await new Promise(r => setTimeout(r, 30));
deserializeWorld(sv.meta, sv.buffers);
}
catch (e) {
toast(e && e.message ? e.message : 'That save could not be loaded.', { big: true, title: 'Save not loaded' });
showLoading(false);
GAME.running = false; $('hud').hidden = true; $('title').hidden = false; if (!TITLE.raf) drawTitle();
return false;
}
GAME.worldId = sv.worldId || ''; GAME.worldName = sv.worldName || '';
GAME.playTime = sv.playTime || 0;
GAME.drag = null; GAME.blow.clear();
onProgress(0.95, 'Your gear and notebook');
await loadCareer();
showLoading(false);
enterGame();
return true;
}
async function loadSavedWorld(id) {
const progress = (f, s) => showLoading(true, f, s, 'Loading world');
progress(0, 'Reading the save');
try {
const sv = id ? await worldNewest(id, progress) : await newestSave(progress);
if (sv) return await startFromSave(sv, progress);
toast('No saved world found.'); return false;
} catch (e) { toast('Could not reach browser storage.'); return false; }
finally { showLoading(false); }
}
async function quitToTitle() {
await saveWorld('main', true);
GAME.running = false; closeAllModals();
@@ -119,7 +143,7 @@ async function renderWorlds(editId) {
button('Save', async () => { await renameWorld(w.id, input.value); await renderWorlds(); await refreshContinue(); });
button('Cancel', () => renderWorlds());
} else {
button('Load', async () => { const sv = await worldNewest(w.id); if (sv && await startFromSave(sv)) closeAllModals(); else if (!sv) toast('No saved world found.'); });
button('Load', async () => { const ok = await loadSavedWorld(w.id); if (ok) closeAllModals(); });
button('Rename', () => renderWorlds(w.id));
button('Delete', () => dialog('Delete world?', `<p class="note">Delete ${esc(w.name)} and its autosave? Exported world files are not affected.</p>`, [
{ label: 'Cancel', cls: 'ghost' },
+1 -1
View File
@@ -38,7 +38,7 @@ const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN
SCRCU, SCRFE, SCRST, SCRSN, SCRZN, SCRAG, SCRAU, SCRBZ, SCRBS, ASH, SLAG, SNOW, WATER, MAGMA, MCU, MFE, MST, MGL, MSN, MZN, MAG, MAU, MBZ, MBS];
const GAME = {
ctrl: false,
running: false, paused: false, tool: 0, blow: new Set(), wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {},
running: false, loading: false, paused: false, tool: 0, blow: new Set(), wallMat: RSTONE, filterCfg: { m: [], inv: 0 }, pieceIdx: 0, pieceDir: {},
brush: [3, 2, 0, 0, 0], tipsOpen: new Set(), cards: [], vacFilter: 0, rmbConsumed: false, rmbSeen: new Set(), lookHold: { x: 0, y: 0 },
paste: false, selDrag: null, rmbRec: null, dragDir: -1, replaceShown: false, clip: null, drag: null, aim: { x: 0, y: 0 }, mouse: { x: 0, y: 0, in: false },
lmb: false, rmb: false, drillW: 1, drillTune: 0, lastDug: 0, padRun: 0, status: '', statusKind: 'err', statusT: 0, usingPad: false, padAim: { x: 12, y: 0 },