/* ============================================================ * 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 13${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);