76 lines
5.3 KiB
JavaScript
76 lines
5.3 KiB
JavaScript
/* Game shell (src/ui/): browser only. This file: settings, the GAME state object, DOM helpers,
|
|
toasts and tool status. */
|
|
|
|
// ---------- 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: 'Pneumatic nozzle', key: '2' },
|
|
{ id: 'build', name: 'Build', key: '3' },
|
|
{ id: 'ignite', name: 'Igniter', key: '4' },
|
|
];
|
|
const ICONS = {
|
|
drill: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M4 9h9l3 3-3 3H4z"/><path d="M16 12h5M17 9.5l3 2.5-3 2.5"/><path d="M7 9v6M10 9v6"/></svg>',
|
|
vacuum: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M3 15l6-6h4l2 2v4l-6 6z"/><path d="M15 11l5-5M17 4l3 3"/><circle cx="6" cy="19" r="0.8" fill="currentColor"/><circle cx="4" cy="21" r="0.6" fill="currentColor"/></svg>',
|
|
build: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="6" width="18" height="12" rx="1"/><path d="M3 12h18M9 6v6M15 12v6"/></svg>',
|
|
ignite: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M12 21c-4 0-6-2.6-6-5.6 0-3.4 3-5 3.6-8.4C12.5 9 14 11 14 13c1-.6 1.6-1.8 1.6-3 2 1.6 2.4 3.6 2.4 5.4 0 3-2 5.6-6 5.6z"/></svg>',
|
|
blueprint: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="11" height="11" rx="1" stroke-dasharray="2.5 2"/><rect x="10" y="10" width="11" height="11" rx="1"/></svg>',
|
|
};
|
|
const BAG_ORDER = [BARAU, BARAG, BARBZ, BARBS, BARST, BARFE, BARCU, BARSN, BARZN, BARGL, BARSB, PLANK, 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 = {
|
|
ctrl: false,
|
|
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');
|
|
let t = [...box.children].find(e => !e.classList.contains('out') && e._text === text && e._title === (opts.title || ''));
|
|
if (!t) {
|
|
while (box.children.length > 4) {
|
|
clearTimeout(box.firstChild._fade); clearTimeout(box.firstChild._remove);
|
|
box.firstChild.remove();
|
|
}
|
|
t = el('div', { class: 'toast plate' + (opts.big ? ' big' : '') + (opts.kind ? ' ' + opts.kind : '') });
|
|
t._text = text; t._title = opts.title || '';
|
|
t.innerHTML = opts.title ? `<b>${esc(opts.title)}</b>${esc(text)}` : esc(text);
|
|
box.appendChild(t);
|
|
}
|
|
clearTimeout(t._fade);
|
|
const life = opts.big ? 5200 : 2600;
|
|
t._fade = setTimeout(() => { t.classList.add('out'); t._remove = setTimeout(() => t.remove(), 450); }, life);
|
|
}
|
|
function setStatus(s, kind, quiet) { if (s) { GAME.status = s; GAME.statusKind = kind || 'err'; GAME.statusT = performance.now(); hudDirty = true; if (!quiet) toast(s, { kind: kind || 'err' }); } }
|