158 lines
9.1 KiB
JavaScript
158 lines
9.1 KiB
JavaScript
// 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(); resetMap();
|
||
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-map', '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-map', 'm-dialog'].forEach(id => $(id).hidden = true); GAME.paused = false; }
|
||
function topModal() { for (const id of ['m-dialog', 'm-map', '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 = '<p class="note">No worlds saved yet. Start a new world.</p>';
|
||
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 = `<div class="world-info"><strong>${esc(w.name)}</strong><div class="note">seed ${esc(w.seed)} · ${esc(w.w)}×${esc(w.h)} grains · ${Math.floor(mins / 60)}:${String(mins % 60).padStart(2, '0')} played</div><div class="note">${w.savedAt ? esc(new Date(w.savedAt).toLocaleString()) : 'Not saved yet'}</div></div>`;
|
||
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?', `<p class="note">Delete ${esc(w.name)} and its autosave? Exported world files are not affected.</p>`, [
|
||
{ 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.'); } };
|