/* Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers, toasts and the status line. */ // ---------- Settings ---------- const SET_DEFAULT = { zoom: 12, integerScale: true, renderScale: 1, uiScale: 1, fpsCap: 0, showPerf: false, shake: true, cameraSmooth: 0.7, cameraLead: true, look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3, volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true, deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: true, sv: 2, }; const SET = Object.assign({}, SET_DEFAULT); // sv: settings version. 2: the default zoom became fully zoomed in, so older saved settings move there once. function loadSettings() { try { const s = JSON.parse(localStorage.getItem('deepgrain.settings') || '{}'); Object.assign(SET, s); if (!(s.sv >= 2)) { SET.zoom = SET_DEFAULT.zoom; SET.sv = 2; if (Object.keys(s).length) saveSettings(); } } catch (e) { /* storage unavailable */ } } function saveSettings() { try { localStorage.setItem('deepgrain.settings', JSON.stringify(SET)); } catch (e) { /* storage unavailable */ } } loadSettings(); // ---------- Game state ---------- const TOOLS = [ { id: 'drill', name: 'Drill', key: '1' }, { id: 'vacuum', name: 'Vacuum', key: '2' }, { id: 'build', name: 'Build', key: '3' }, { id: 'ignite', name: 'Igniter', key: '4' }, ]; const ICONS = { drill: '', vacuum: '', build: '', ignite: '', blueprint: '', }; const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN, BARGL, BARSB, AUORE, AGORE, ZNORE, SNORE, RSTONE, LCLAY, RBRICK, LDIRT, LWOOD, ACORN, COAL, CHARCOAL, CUORE, FEORE, SAND, GRAVEL, RGRAN, RBAS, CULLET, 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 = { running: 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 }, lastAutosave: 0, playTime: 0, lastHud: 0, worldId: '', worldName: '', }; const keys = new Set(); let hudDirty = true; // ---------- DOM helpers ---------- const $ = id => document.getElementById(id); function el(tag, attrs, html) { const e = document.createElement(tag); if (attrs) for (const k in attrs) e.setAttribute(k, attrs[k]); if (html !== undefined) e.innerHTML = html; return e; } function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); } function rgbOf(m) { return `rgb(${COLR[m]},${COLG[m]},${COLB[m]})`; } function fmtT(t) { return t >= 999.5 ? `${Math.round(t).toLocaleString('en-US')}°C` : `${Math.round(t)}°C`; } // ---------- Toasts ---------- function toast(text, opts = {}) { const box = $('toasts'); while (box.children.length > 4) box.firstChild.remove(); const t = el('div', { class: 'toast plate' + (opts.big ? ' big' : '') }); t.innerHTML = opts.title ? `${esc(opts.title)}${esc(text)}` : esc(text); box.appendChild(t); const life = opts.big ? 5200 : 2600; setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 450); }, life); } function setStatus(s, kind) { if (s) { GAME.status = s; GAME.statusKind = kind || 'err'; GAME.statusT = performance.now(); hudDirty = true; } }