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:
+8
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user