/* ============================================================ * Diablo2D โ hud.js : UI core + in-game HUD (orbs, hotbar, minimap) * ============================================================ */ 'use strict'; window.D2 = window.D2 || {}; (function (D2) { const ui = {}; D2.ui = ui; let el = {}; let minimapCtx = null; let _minimapT = 0; let hotbarSlots = []; // {slotEl, cdEl, iconKey} let toastCount = 0; /* ---------------- init ---------------- */ ui.init = function () { el = { hud: document.getElementById('hud'), lifeFill: document.getElementById('life-fill'), manaFill: document.getElementById('mana-fill'), lifeText: document.getElementById('life-text'), manaText: document.getElementById('mana-text'), xpBar: document.getElementById('xp-bar'), xpText: document.getElementById('xp-text'), lvlNum: document.getElementById('lvl-num'), hbSkills: document.getElementById('hb-skills'), hbPotions: document.getElementById('hb-potions'), hbMenus: document.getElementById('hb-menus'), buffBar: document.getElementById('buff-bar'), toasts: document.getElementById('toasts'), minimap: document.getElementById('minimap'), minimapLabel: document.getElementById('minimap-label'), bossWrap: document.getElementById('boss-bar-wrap'), bossName: document.getElementById('boss-name'), bossFill: document.getElementById('boss-fill'), questTracker: document.getElementById('quest-tracker'), }; minimapCtx = el.minimap.getContext('2d'); buildHotbar(); D2.i18n.applyDom(document); }; ui.showHud = function () { el.hud.classList.remove('hidden'); }; ui.hideHud = function () { el.hud.classList.add('hidden'); }; /* ---------------- hotbar ---------------- */ function buildHotbar() { el.hbSkills.innerHTML = ''; el.hbPotions.innerHTML = ''; el.hbMenus.innerHTML = ''; hotbarSlots = []; /* skill slots: LMB basic, RMB, 1..4 */ const defs = [ { slot: 0, label: 'LMB' }, { slot: 1, label: 'RMB' }, { slot: 2, label: '1' }, { slot: 3, label: '2' }, { slot: 4, label: '3' }, ]; for (const d of defs) { const s = document.createElement('div'); s.className = 'hb-slot'; s.innerHTML = `${d.label}
`; s.addEventListener('mousedown', (e) => { e.stopPropagation(); if (d.slot === 0) return; /* clicking a slot casts it toward screen center-ish cursor */ D2.game.castHotbarFromUi && D2.game.castHotbarFromUi(d.slot); }); el.hbSkills.appendChild(s); hotbarSlots.push({ slot: d.slot, root: s, canvas: s.querySelector('canvas'), cd: s.querySelector('.cd-overlay'), iconKey: null, }); } /* potions */ const pots = [ { type: 'hp', label: 'Q', icon: 'potion_hp', tip: 'hb.health_potion' }, { type: 'mp', label: 'E', icon: 'potion_mp', tip: 'hb.mana_potion' }, ]; for (const pd of pots) { const s = document.createElement('div'); s.className = 'hb-slot'; s.title = D2.i18n.t(pd.tip); s.dataset.potion = pd.type; s.innerHTML = `${pd.label}0`; s.querySelector('canvas').getContext('2d').drawImage( D2.sprites.skillIconCanvas(pd.icon, 48), 0, 0, 36, 36); s.addEventListener('mousedown', (e) => { e.stopPropagation(); D2.player.usePotion(D2.game, pd.type); }); el.hbPotions.appendChild(s); } /* menu buttons */ const menus = [ { key: 'KeyI', icon: null, glyph: '๐', tip: 'hb.inventory', act: () => ui.togglePanel('inventory') }, { key: 'KeyC', icon: null, glyph: '๐ช', tip: 'hb.character', act: () => ui.togglePanel('character') }, { key: 'KeyT', icon: null, glyph: 'โจ', tip: 'hb.skills', act: () => ui.togglePanel('skills') }, { key: 'KeyJ', icon: null, glyph: '๐', tip: 'hb.quests', act: () => ui.togglePanel('quests') }, { key: 'F1', icon: null, glyph: '?', tip: 'hb.help', act: () => D2.main && D2.main.openHelp() }, ]; for (const md of menus) { const s = document.createElement('div'); s.className = 'hb-slot small'; s.title = D2.i18n.t(md.tip); s.innerHTML = `${md.glyph}`; s.addEventListener('mousedown', (e) => { e.stopPropagation(); md.act(); }); el.hbMenus.appendChild(s); } } ui.refreshHotbar = function () { const G = D2.game; const p = G.player; if (!p) return; for (const hs of hotbarSlots) { const skillId = p.hotbar[hs.slot]; let iconId = null; if (skillId === 'basic') { iconId = 'basic_' + (p.classId === 'crusader' ? 'melee' : p.classId === 'ranger' ? 'ranged' : 'cast'); } else if (skillId) { const sk = D2.Skills.findSkill(p.classId, skillId); iconId = sk ? sk.icon : null; } if (iconId && iconId !== hs.iconKey) { const c = hs.canvas.getContext('2d'); c.clearRect(0, 0, 36, 36); c.drawImage(D2.sprites.skillIconCanvas(iconId, 48), 0, 0, 36, 36); hs.iconKey = iconId; } hs.root.classList.toggle('empty-slot', !iconId); /* dim if unlearned */ if (skillId && skillId !== 'basic') { const rank = D2.player.getSkillRank(p, skillId); hs.root.style.opacity = rank <= 0 ? 0.45 : 1; } else hs.root.style.opacity = 1; } /* potion counts */ el.hbPotions.querySelectorAll('[data-potion]').forEach(n => { const t = n.dataset.potion; n.querySelector('.potion-count').textContent = p.potions[t]; n.style.opacity = p.potions[t] > 0 ? 1 : 0.4; }); }; /* ---------------- per-frame HUD ---------------- */ ui.tick = function (dt) { const G = D2.game; if (!G.player || G.state === 'title') return; /* cooldown sweeps */ for (const hs of hotbarSlots) { const skillId = G.player.hotbar[hs.slot]; if (!skillId || skillId === 'basic') { hs.cd.style.display = 'none'; continue; } const left = G.skillCooldownLeft(skillId); const sk = D2.Skills.findSkill(G.player.classId, skillId); const total = sk ? Math.max(0.01, sk.cooldown) : 1; if (left > 0) { const pct = Math.min(1, left / total); const deg = 360 * (1 - pct); hs.cd.style.display = 'block'; hs.cd.style.background = `conic-gradient(rgba(0,0,0,.78) ${deg}deg, transparent ${deg}deg)`; hs.cd.textContent = left > 1 ? Math.ceil(left) : ''; hs.cd.style.color = '#fff'; hs.cd.style.fontSize = '14px'; hs.cd.style.display = 'flex'; hs.cd.style.alignItems = 'center'; hs.cd.style.justifyContent = 'center'; } else if (hs.cd.style.display !== 'none') { hs.cd.style.display = 'none'; } } /* buffs */ if (G.player.buffs.length !== ui._lastBuffCount) { ui._lastBuffCount = G.player.buffs.length; rebuildBuffs(); } for (const b of G.player.buffs) { if (b.timerEl) b.timerEl.textContent = Math.ceil(b.t); } /* minimap @ ~7fps */ _minimapT -= dt; if (_minimapT <= 0) { _minimapT = 0.14; D2.render.minimap(G, el.minimap); el.minimapLabel.textContent = G.world.isTown ? D2.i18n.t('town.enter').split('โ')[0].trim() : `${G.world.act >= 0 ? D2.i18n.t(D2.BAL.acts[G.world.act].name) : ''} ยท ${D2.i18n.t('floor.depth', G.world.floorIdx + 1)}`; } }; function rebuildBuffs() { el.buffBar.innerHTML = ''; for (const b of D2.game.player.buffs) { const d = document.createElement('div'); d.className = 'buff-icon'; d.title = b.name ? (b.name[D2.i18n.getLang()] || b.name.en) : b.id; const cv = document.createElement('canvas'); cv.width = 40; cv.height = 40; cv.getContext('2d').drawImage(D2.sprites.skillIconCanvas(b.icon || 'renewal', 48), 2, 2, 36, 36); d.appendChild(cv); const t = document.createElement('div'); t.className = 'buff-timer'; d.appendChild(t); b.timerEl = t; el.buffBar.appendChild(d); } } /* ---------------- periodic refresh ---------------- */ ui.refreshHud = function () { const G = D2.game; const p = G.player; if (!p) return; el.lifeFill.style.height = Math.max(0, p.hp / p.maxHp * 100) + '%'; el.manaFill.style.height = Math.max(0, p.mana / p.maxMana * 100) + '%'; el.lifeText.textContent = `${Math.ceil(p.hp)}/${p.maxHp}`; el.manaText.textContent = `${Math.ceil(p.mana)}/${p.maxMana}`; const need = D2.BAL.xpForLevel(p.level); const prev = p.level > 1 ? D2.BAL.xpForLevel(p.level - 1) : 0; const pct = D2.util.clamp((p.xp - prev) / Math.max(1, need - prev), 0, 1); el.xpBar.style.width = (pct * 100) + '%'; el.xpText.textContent = `XP ${D2.util.fmtNum(p.xp)} / ${D2.util.fmtNum(need)}${p.statPoints ? ` ยท +${p.statPoints}${' '}` : ''}`; el.lvlNum.textContent = 'Lv ' + p.level + (G.progress.torment ? ' ยท ' + D2.i18n.t('torment', G.progress.torment) : ''); ui.refreshHotbar(); }; /* ---------------- toasts ---------------- */ ui.toast = function (text, cls = '') { if (!el.toasts) return; const d = document.createElement('div'); d.className = 'toast ' + cls; d.textContent = text; el.toasts.appendChild(d); toastCount++; setTimeout(() => d.classList.add('fadeout'), 2200); setTimeout(() => { d.remove(); toastCount--; }, 2800); while (el.toasts.children.length > 4) el.toasts.firstChild.remove(); }; /* ---------------- boss bar ---------------- */ ui.showBossBar = function (name) { el.bossName.textContent = name; el.bossFill.style.width = '100%'; el.bossWrap.classList.remove('hidden'); }; ui.updateBossBar = function (pct) { el.bossFill.style.width = Math.max(0, pct * 100) + '%'; }; ui.hideBossBar = function () { el.bossWrap.classList.add('hidden'); }; /* ---------------- quest tracker ---------------- */ ui.renderQuestTracker = function () { const G = D2.game; let html = '