/* ============================================================
* Diablo2D — screens.js : title, class select, settings, help,
* death & victory screens
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
const ui = D2.ui;
const $ = id => document.getElementById(id);
function show(id) {
for (const sid of ['screen-loading', 'screen-title', 'screen-classselect', 'screen-settings', 'screen-help', 'screen-death', 'screen-victory']) {
$(sid).classList.toggle('hidden', sid !== id);
}
}
ui.hideScreens = () => show(null);
/* ================= loading ================= */
const TIP_KEYS = ['loading.tips.0', 'loading.tips.1', 'loading.tips.2', 'loading.tips.3', 'loading.tips.4'];
ui.showLoading = function () {
show('screen-loading');
$('loading-tip').textContent = D2.i18n.t(TIP_KEYS[(Math.random() * TIP_KEYS.length) | 0]);
};
ui.setLoadingProgress = function (p) {
$('loading-progress').style.width = Math.round(p * 100) + '%';
};
/* ================= title ================= */
ui.showTitle = function () {
show('screen-title');
const scr = $('screen-title');
const G = D2.game;
const meta = D2.save.readMeta();
scr.innerHTML = `
DIABLO2D
${D2.i18n.t('title.tagline')}
${meta ? `
${D2.i18n.t('title.saved_hero')}: ${D2.i18n.t('class.' + meta.classId + '.name')} · ${D2.i18n.t('level_short')} ${meta.level}${meta.torment ? ' · ' + D2.i18n.t('torment', meta.torment) : ''}${meta.hardcore ? ' · ☠ Hardcore' : ''}
` : ''}
`;
scr.querySelector('#t-new').addEventListener('click', () => {
if (meta && !confirm(D2.i18n.t('title.confirm_overwrite'))) return;
D2.main.openClassSelect();
});
scr.querySelector('#t-continue').addEventListener('click', () => {
D2.main.startContinue();
});
scr.querySelector('#t-settings').addEventListener('click', () => {
ui.showSettings(() => ui.showTitle());
});
scr.querySelector('#t-help').addEventListener('click', () => {
ui.showHelp(() => ui.showTitle());
});
D2.audio.playMusic('title');
};
/* ================= class select ================= */
ui.showClassSelect = function () {
show('screen-classselect');
const scr = $('screen-classselect');
scr.innerHTML = `
${D2.i18n.t('class.select')}
`;
const grid = scr.querySelector('.class-grid');
let selected = null;
for (const clsId of ['crusader', 'ranger', 'sorceress']) {
const cls = D2.Skills.CLASSES[clsId];
const card = document.createElement('div');
card.className = 'class-card';
card.innerHTML = `
${D2.i18n.t('class.' + clsId + '.name')}
${D2.i18n.t(cls.descKey)}
⚔ ${cls.baseMods.str} STR
🏹 ${cls.baseMods.dex} DEX
❤ ${cls.baseMods.vit} VIT
✦ ${cls.baseMods.ene} ENE
`;
drawClassPortrait(card.querySelector('canvas'), cls);
card.addEventListener('click', () => {
grid.querySelectorAll('.class-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selected = clsId;
scr.querySelector('#cs-play').disabled = false;
D2.audio.sfx('click');
});
card.addEventListener('dblclick', () => {
selected = clsId;
D2.main.startNewGame(clsId);
});
grid.appendChild(card);
}
scr.querySelector('#cs-play').addEventListener('click', () => {
if (selected) D2.main.startNewGame(selected);
});
scr.querySelector('#cs-back').addEventListener('click', () => ui.showTitle());
};
function drawClassPortrait(cv, cls) {
const x = cv.getContext('2d');
const W = cv.width, H = cv.height;
/* bg */
const g = x.createRadialGradient(W / 2, H * 0.42, 8, W / 2, H / 2, W * 0.62);
g.addColorStop(0, 'rgba(70,55,35,.55)');
g.addColorStop(1, 'rgba(8,6,4,.9)');
x.fillStyle = g;
x.fillRect(0, 0, W, H);
/* ring */
x.strokeStyle = cls.palette.accent;
x.lineWidth = 3;
x.beginPath(); x.arc(W / 2, H * 0.46, 52, 0, Math.PI * 2); x.stroke();
x.globalAlpha = 0.35;
x.beginPath(); x.arc(W / 2, H * 0.46, 60, 0, Math.PI * 2); x.stroke();
x.globalAlpha = 1;
/* figure: simple hooded silhouette in class colors */
x.save();
x.translate(W / 2, H * 0.46);
const pal = cls.palette;
/* cloak */
x.fillStyle = pal.cloth;
x.beginPath();
x.moveTo(0, -44);
x.quadraticCurveTo(-38, -8, -30, 44);
x.lineTo(30, 44);
x.quadraticCurveTo(38, -8, 0, -44);
x.fill();
/* head */
x.fillStyle = '#1a1410';
x.beginPath(); x.arc(0, -22, 15, 0, Math.PI * 2); x.fill();
/* eyes glow */
x.fillStyle = pal.accent;
x.fillRect(-8, -26, 5, 3.5);
x.fillRect(3, -26, 5, 3.5);
/* emblem weapon */
x.restore();
const wepBaseId = cls.id === 'crusader' ? 'longsword' : cls.id === 'ranger' ? 'huntingbow' : 'runestaff';
const fakeItem = { slot: 'weapon', baseId: wepBaseId, kind: cls.weaponKind, rarity: 'legendary' };
const icon = D2.sprites.itemIconCanvas(fakeItem, 48);
x.drawImage(icon, W / 2 - 24, H * 0.46 - 24, 48, 48);
}
/* ================= settings ================= */
ui.showSettings = function (onBack) {
show('screen-settings');
const scr = $('screen-settings');
const G = D2.game;
const s = G.settings;
scr.innerHTML = `
${D2.i18n.t('title.settings')}
`;
const bind = (id, key) => {
scr.querySelector(id).addEventListener('input', (e) => {
s[key] = parseFloat(e.target.value);
G.applySettings();
D2.save.set(D2.save.KEY.SETTINGS, s);
});
};
bind('#s-master', 'masterVol');
bind('#s-music', 'musicVol');
bind('#s-sfx', 'sfxVol');
scr.querySelector('#s-lang').addEventListener('change', (e) => {
s.lang = e.target.value;
G.applySettings();
D2.save.set(D2.save.KEY.SETTINGS, s);
ui.showSettings(onBack); // rebuild with new language
});
const toggle = (id, key) => {
scr.querySelector(id).addEventListener('click', (e) => {
s[key] = !s[key];
e.target.classList.toggle('on', s[key]);
G.applySettings();
D2.save.set(D2.save.KEY.SETTINGS, s);
});
};
toggle('#s-shake', 'screenShake');
toggle('#s-dmg', 'dmgNumbers');
scr.querySelector('#s-labels').addEventListener('change', (e) => {
s.labels = e.target.value;
D2.save.set(D2.save.KEY.SETTINGS, s);
});
scr.querySelector('#s-export').addEventListener('click', () => {
const snap = D2.save.serializeSnapshot();
const code = btoa(unescape(encodeURIComponent(snap)));
const ta = scr.querySelector('#s-code');
ta.value = code;
ta.select();
try {
navigator.clipboard.writeText(code);
ui.toast(D2.i18n.t('set.copy_ok'));
} catch (e) { /* selection is enough */ }
});
scr.querySelector('#s-import').addEventListener('click', () => {
const code = scr.querySelector('#s-code').value.trim();
if (!code) return;
try {
const json = decodeURIComponent(escape(atob(code)));
if (D2.save.restoreSnapshot(json)) {
ui.toast(D2.i18n.t('set.import_ok'));
} else ui.toast(D2.i18n.t('set.import_bad'), 'bad');
} catch (e) {
ui.toast(D2.i18n.t('set.import_bad'), 'bad');
}
});
scr.querySelector('#s-wipe').addEventListener('click', () => {
if (confirm(D2.i18n.t('set.confirm_wipe'))) {
D2.save.wipeAll();
ui.toast('OK');
}
});
scr.querySelector('#s-back').addEventListener('click', () => onBack());
};
/* ================= help ================= */
ui.showHelp = function (onBack) {
show('screen-help');
const scr = $('screen-help');
scr.innerHTML = `
${D2.i18n.t('ui.help')}
${D2.i18n.t('help.keys_title')}
LMB${D2.i18n.t('help.move')}
RMB 1–3${D2.i18n.t('help.combat')}
Q E${D2.i18n.t('hb.health_potion')} / ${D2.i18n.t('hb.mana_potion')}
F/SpaceInteract — stairs, chests, shrines, NPCs
Alt${D2.i18n.t('help.items')}
I C T J M${D2.i18n.t('help.panels')}
Tips
${D2.i18n.t('help.tip1')}
${D2.i18n.t('help.tip2')}
${D2.i18n.t('help.tip3')}
Credits
${D2.i18n.t('credits.line1')}
${D2.i18n.t('credits.line2')}
`;
scr.querySelector('#h-back').addEventListener('click', () => onBack());
};
/* ================= death ================= */
ui.showDeath = function () {
show('screen-death');
const scr = $('screen-death');
scr.innerHTML = `
${D2.i18n.t('death.title')}
${D2.i18n.t('death.subtitle')}
`;
scr.querySelector('#d-respawn').addEventListener('click', () => {
ui.hideScreens();
D2.game.respawnInTown();
});
scr.querySelector('#d-title').addEventListener('click', () => {
D2.game.saveGame();
D2.game.state = 'title';
ui.showTitle();
});
};
/* ================= victory ================= */
ui.showVictory = function () {
show('screen-victory');
const scr = $('screen-victory');
const G = D2.game;
const p = G.player;
scr.innerHTML = `
${D2.i18n.t('victory.title')}
${D2.i18n.t('victory.subtitle')}
${D2.i18n.t('victory.stats')}
${D2.i18n.t('level_short')} ${p.level} · ${p.kills} kills · ${p.eliteKills} elites · ${p.bossKills} bosses · ${p.deaths} deaths
${G.progress.torment < D2.BAL.maxTorment ? `` : ''}
`;
const vt = scr.querySelector('#v-torment');
if (vt) vt.addEventListener('click', () => {
G.progress.torment++;
G.progress.act = 0;
G.progress.floorIdx = 0;
G.victoryShown = false;
G.saveGame();
ui.hideScreens();
G.enterTown();
G.state = 'playing';
ui.toast(D2.i18n.t('torment', G.progress.torment), 'gold');
});
scr.querySelector('#v-title').addEventListener('click', () => {
G.saveGame();
G.state = 'title';
ui.showTitle();
});
};
})(window.D2);