Files
deepgrain-foundry/src/ui/session.js
T

158 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.'); } };