// Starting, loading and quitting a game; modal dialogs; the pause menu. // ---------- Start / stop ---------- 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) + '%'; } } function enterGame() { $('cards').innerHTML = ''; if (R.floaters) R.floaters.length = 0; R.particles.length = 0; GAME.status = ''; GAME.statusT = 0; $('title').hidden = true; $('hud').hidden = false; GAME.running = true; GAME.paused = false; buildSky(); resizeRender(); snapCamera(); GAME.lastAutosave = performance.now(); GAME.drag = null; GAME.lmb = GAME.rmb = false; lastBagKey = ''; hudDirty = true; renderHotbar(); } 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; 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); } async function startNewWorld(seed, w, h, name = '') { try { const worlds = await listWorlds(); await setupNewWorld(seed, w, h, cleanWorldName(name, claimName(seed, worlds.map(w => w.name)))); 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, 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(); $('hud').hidden = true; $('title').hidden = false; refreshContinue(); if (!TITLE.raf) drawTitle(); } async function refreshContinue() { const sv = await newestSave(); $('btn-worlds').hidden = !(await listWorlds()).length; const b = $('btn-continue'); if (sv && sv.meta && sv.meta.v === SAVE_VERSION) { b.hidden = false; const d = new Date(sv.savedAt); $('continue-meta').textContent = `${sv.worldName} · ${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`; } else b.hidden = true; } // ---------- Modals ---------- function anyModal() { return ['m-pause', 'm-settings', 'm-notebook', 'm-worlds', 'm-dialog'].some(id => !$(id).hidden); } function openModal(id) { $(id).hidden = false; GAME.paused = true; setTimeout(() => { const f = $(id).querySelector('button, select, input'); if (f && !GAME.usingPadMenu) { /* keep focus neutral for mouse users */ } }, 0); } function closeModal(id) { $(id).hidden = true; if (id === 'm-dialog') GAME.editRun = null; if (!anyModal()) GAME.paused = false; } function closeAllModals() { ['m-pause', 'm-settings', 'm-notebook', 'm-worlds', 'm-dialog'].forEach(id => $(id).hidden = true); GAME.paused = false; } function topModal() { for (const id of ['m-dialog', 'm-worlds', 'm-settings', 'm-notebook', 'm-pause']) if (!$(id).hidden) return id; return null; } function dialog(title, bodyHtml, buttons) { $('dlg-title').textContent = title; $('dlg-body').innerHTML = bodyHtml; const foot = $('dlg-foot'); foot.innerHTML = ''; for (const b of buttons) { const btn = el('button', { class: 'btn ' + (b.cls || '') }, esc(b.label)); btn.onclick = () => { playSfx('ui'); const keep = b.fn && b.fn(); if (!keep) closeModal('m-dialog'); }; foot.appendChild(btn); } openModal('m-dialog'); } document.querySelectorAll('[data-close]').forEach(b => b.addEventListener('click', () => { playSfx('ui'); closeModal(b.closest('.modal').id); })); // pause menu $('pm-resume').onclick = () => { playSfx('ui'); closeModal('m-pause'); }; $('pm-notebook').onclick = () => { playSfx('ui'); closeModal('m-pause'); openNotebook(); }; $('pm-settings').onclick = () => { playSfx('ui'); openSettings(); }; $('pm-save').onclick = () => { playSfx('ui'); saveWorld('main'); }; $('pm-export').onclick = () => { playSfx('ui'); exportWorld(); }; $('pm-import').onclick = () => { playSfx('ui'); $('importfile').click(); }; $('importfile').onchange = e => { const f = e.target.files[0]; if (f) { closeAllModals(); importWorldFile(f); } e.target.value = ''; }; $('pm-title').onclick = () => { playSfx('ui'); quitToTitle(); }; function openPause() { $('pause-meta').textContent = `${GAME.worldName} · seed ${worldSeed} · ${W}×${H}`; openModal('m-pause'); } // ---------- World list ---------- async function renderWorlds(editId) { const list = await listWorlds(), body = $('worlds-list'); body.innerHTML = ''; if (!list.length) body.innerHTML = '

No worlds saved yet. Start a new world.

'; for (const w of list) { const row = el('div', { class: 'world-row', 'data-world': w.id }); const mins = Math.floor((w.playTime || 0) / 60); row.innerHTML = `
${esc(w.name)}
seed ${esc(w.seed)} · ${esc(w.w)}×${esc(w.h)} grains · ${Math.floor(mins / 60)}:${String(mins % 60).padStart(2, '0')} played
${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Not saved yet'}
`; const actions = el('div', { class: 'world-actions' }); const button = (label, fn, cls = '') => { const b = el('button', { class: 'btn ' + cls }, label); b.onclick = async () => { try { await fn(); } catch (e) { toast('Could not reach browser storage.'); } }; actions.appendChild(b); }; if (editId === w.id) { const input = el('input', { type: 'text', maxlength: 40, 'aria-label': 'World name' }); input.value = w.name; actions.appendChild(input); button('Save', async () => { await renameWorld(w.id, input.value); await renderWorlds(); await refreshContinue(); }); button('Cancel', () => renderWorlds()); } else { button('Load', async () => { const ok = await loadSavedWorld(w.id); if (ok) closeAllModals(); }); button('Rename', () => renderWorlds(w.id)); button('Delete', () => dialog('Delete world?', `

Delete ${esc(w.name)} and its autosave? Exported world files are not affected.

`, [ { label: 'Cancel', cls: 'ghost' }, { label: 'Delete', cls: 'danger', fn: async () => { try { await deleteWorld(w.id); closeModal('m-dialog'); await renderWorlds(); await refreshContinue(); } catch (e) { toast('Could not delete the world.'); } } }, ]), 'danger'); } row.appendChild(actions); body.appendChild(row); } if (editId) { const input = body.querySelector('input'); if (input) { input.focus(); input.select(); } } } $('btn-worlds').onclick = async () => { try { await renderWorlds(); openModal('m-worlds'); } catch (e) { toast('Could not reach browser storage.'); } };