Files
deepseek fc1fa2d51e Diablo2D — Shadows of Tristram: complete browser ARPG
- 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
2026-08-23 06:59:36 +00:00

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);