- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
185 lines
5.8 KiB
JavaScript
185 lines
5.8 KiB
JavaScript
/* ============================================================
|
|
* 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);
|