/* ============================================================ * Diablo2D — main.js : bootstrap & game loop * ============================================================ */ 'use strict'; window.D2 = window.D2 || {}; (function (D2) { let lastT = 0; let hudRefreshT = 0; const G = D2.game; /* ---------------- pause ---------------- */ function togglePause() { const overlay = document.getElementById('pause-overlay'); if (G.state === 'playing') { G.state = 'paused'; overlay.classList.remove('hidden'); D2.audio.stopMusic(0.3); } else if (G.state === 'paused') { G.state = 'playing'; overlay.classList.add('hidden'); if (G.world) D2.audio.playMusic(G.world.isTown ? 'town' : (G.world.isBossLair ? 'boss' : G.world.theme.music)); } } function exitToTitle() { G.saveGame(); G.state = 'title'; document.getElementById('pause-overlay').classList.add('hidden'); D2.ui.closeAllPanels(); D2.ui.hideHud(); D2.ui.showTitle(); } function openHelpInGame() { G.state = 'paused'; document.getElementById('pause-overlay').classList.add('hidden'); D2.ui.showHelp(() => { if (G.player && !G.player.dead) { G.state = 'playing'; D2.ui.hideScreens(); } else { D2.ui.showTitle(); } }); } /* ---------------- boot ---------------- */ function boot() { /* load persisted settings before anything uses them */ const savedSettings = D2.save.get(D2.save.KEY.SETTINGS); if (savedSettings) Object.assign(G.settings, savedSettings); D2.i18n.setLang(G.settings.lang); const canvas = document.getElementById('game-canvas'); D2.render.init(canvas); D2.input.init(canvas); D2.audio.playMusic('title'); // becomes pending until first gesture /* staged loading for the progress bar feel */ D2.ui.showLoading(); const steps = [ ['Carving tiles…', () => { D2.sprites.init(); }], ['Binding sigils…', () => { D2.ui.init(); D2.ui.initPanels(); }], ['Raising the dead…', () => { D2.game.updateQuests(); }], ['Opening the gates…', () => {}], ]; let i = 0; const stepFn = () => { if (i < steps.length) { const [label, fn] = steps[i]; try { fn(); } catch (e) { console.error('boot step failed:', label, e); } i++; D2.ui.setLoadingProgress(i / steps.length); setTimeout(stepFn, 120); } else { finishBoot(); } }; setTimeout(stepFn, 200); } function finishBoot() { wirePauseMenu(); window.D2.main = api; // expose after full init G.applySettings(); /* click anywhere unlocks audio */ const unlock = () => { D2.audio.unlock(); }; window.addEventListener('mousedown', unlock, { once: true }); window.addEventListener('keydown', unlock, { once: true }); /* autosave on tab close while playing */ window.addEventListener('beforeunload', () => { if ((G.state === 'playing' || G.state === 'paused') && G.player) G.saveGame(); }); document.addEventListener('visibilitychange', () => { if (document.hidden && G.state === 'playing') togglePause(); }); G.state = 'title'; D2.ui.showTitle(); requestAnimationFrame(loop); } function wirePauseMenu() { document.getElementById('btn-resume').addEventListener('click', togglePause); document.getElementById('btn-pause-settings').addEventListener('click', () => { D2.ui.showSettings(() => { document.getElementById('screen-settings').classList.add('hidden'); if (G.state === 'paused') document.getElementById('pause-overlay').classList.remove('hidden'); }); document.getElementById('pause-overlay').classList.add('hidden'); }); document.getElementById('btn-pause-help').addEventListener('click', () => { document.getElementById('pause-overlay').classList.add('hidden'); D2.ui.showHelp(() => { document.getElementById('screen-help').classList.add('hidden'); document.getElementById('pause-overlay').classList.remove('hidden'); }); }); document.getElementById('btn-save-exit').addEventListener('click', exitToTitle); } /* ---------------- main loop ---------------- */ function loop(t) { requestAnimationFrame(loop); const dt = Math.min(0.05, (t - lastT) / 1000 || 0.016); lastT = t; /* consume this frame's inputs first, act on them, clear edges LAST */ D2.ui.tickGlobalKeys(); if (G.state === 'playing') { G.update(dt); D2.render.frame(G, dt); D2.ui.tick(dt); hudRefreshT -= dt; if (hudRefreshT <= 0) { hudRefreshT = 0.12; D2.ui.refreshHud(); } } else if (G.state === 'paused' || G.state === 'dead' || G.state === 'victory') { /* keep rendering the frozen world behind overlays */ if (G.world) D2.render.frame(G, 0); } D2.input.endFrame(); } /* ---------------- public api ---------------- */ const api = { togglePause, exitToTitle, openHelp: openHelpInGame, openClassSelect() { D2.ui.showClassSelect(); }, startNewGame(classId) { D2.ui.hideScreens(); D2.ui.hideHud ? null : 0; D2.ui.showHud(); G.startNewGame(classId); D2.ui.refreshHud(); D2.ui.renderQuestTracker(); D2.ui.toast(D2.i18n.t('class.' + classId + '.name'), 'gold'); setTimeout(() => { D2.ui.toast(D2.i18n.getLang() === 'vi' ? '🗣 Hãy nói chuyện với Kashya & Cain trong trại (phím F)' : '🗣 Speak with Kashya & Cain in camp (press F)', 'gold'); }, 1200); }, startContinue() { D2.ui.hideScreens(); D2.ui.showHud(); if (G.continueGame()) { D2.ui.refreshHud(); D2.ui.renderQuestTracker(); } }, }; window.addEventListener('DOMContentLoaded', boot); })(window.D2);