/** * RbxlHudOverlay — DOM-оверлей с HUD-элементами для импортированных * Roblox-карт: KillFeed (правый верх), Message/Hint (центр), WinGui. * * Один контейнер `.rbxl-hud-overlay` на canvas.parentElement, в нём дочерние * блоки по типу. Стили inline, ничего не зависит от CSS приложения. * * API: * const hud = new RbxlHudOverlay(canvasParent); * hud.addKillFeed(killer, victim, weapon) * hud.showMessage(text, opts) * hud.hideMessage() * hud.showWin(text) * hud.dispose() */ export class RbxlHudOverlay { constructor(parent) { this._parent = parent || document.body; this._root = null; this._killFeed = null; this._message = null; this._winBox = null; this._killEntries = []; // [{el, expireAt}] this._mount(); } _mount() { if (this._root) return; const root = document.createElement('div'); root.className = 'rbxl-hud-overlay'; Object.assign(root.style, { position: 'absolute', inset: '0', pointerEvents: 'none', zIndex: '999', fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif', }); this._parent.appendChild(root); this._root = root; // KillFeed — правый верхний угол const kf = document.createElement('div'); Object.assign(kf.style, { position: 'absolute', top: '60px', right: '12px', display: 'flex', flexDirection: 'column', gap: '6px', maxWidth: '320px', pointerEvents: 'none', }); root.appendChild(kf); this._killFeed = kf; // Message — центр сверху (Roblox Message по центру экрана, // но в верхней трети чтобы не мешать игре) const msg = document.createElement('div'); Object.assign(msg.style, { position: 'absolute', top: '15%', left: '50%', transform: 'translateX(-50%)', padding: '10px 24px', background: 'rgba(0,0,0,0.6)', color: '#fff', fontSize: '22px', fontWeight: '600', borderRadius: '6px', textShadow: '0 2px 4px rgba(0,0,0,0.8)', display: 'none', pointerEvents: 'none', }); root.appendChild(msg); this._message = msg; // WinGui — большая надпись по центру const win = document.createElement('div'); Object.assign(win.style, { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', padding: '24px 48px', background: 'rgba(0,0,0,0.75)', color: '#ffd86b', fontSize: '48px', fontWeight: '800', borderRadius: '12px', textShadow: '0 4px 8px rgba(0,0,0,0.8)', display: 'none', pointerEvents: 'none', }); root.appendChild(win); this._winBox = win; // Тик для авто-исчезновения KillFeed entries (через 5с) this._tickInterval = setInterval(() => this._cleanupKills(), 500); } addKillFeed(killer, victim, weapon) { if (!this._killFeed) return; const entry = document.createElement('div'); Object.assign(entry.style, { background: 'rgba(0,0,0,0.55)', color: '#fff', padding: '6px 10px', borderRadius: '4px', fontSize: '13px', display: 'flex', gap: '6px', alignItems: 'center', animation: 'rbxlHudFadeIn 0.3s', }); const killerEl = document.createElement('span'); killerEl.textContent = String(killer || '?'); killerEl.style.color = '#5bd1e8'; const arrow = document.createElement('span'); arrow.textContent = weapon ? `→ [${weapon}] →` : '→'; arrow.style.color = '#ff9a52'; const victimEl = document.createElement('span'); victimEl.textContent = String(victim || '?'); victimEl.style.color = '#f87a7a'; entry.appendChild(killerEl); entry.appendChild(arrow); entry.appendChild(victimEl); this._killFeed.appendChild(entry); this._killEntries.push({ el: entry, expireAt: performance.now() + 5000 }); // Keep only last 8 while (this._killEntries.length > 8) { const old = this._killEntries.shift(); try { old.el.remove(); } catch (_) {} } } _cleanupKills() { const now = performance.now(); const keep = []; for (const e of this._killEntries) { if (e.expireAt < now) { try { e.el.remove(); } catch (_) {} } else keep.push(e); } this._killEntries = keep; } showMessage(text, opts = {}) { if (!this._message) return; this._message.textContent = String(text || ''); this._message.style.display = text ? 'block' : 'none'; if (opts.duration) { clearTimeout(this._msgTimer); this._msgTimer = setTimeout(() => this.hideMessage(), opts.duration); } } hideMessage() { if (this._message) this._message.style.display = 'none'; } showWin(text) { if (!this._winBox) return; this._winBox.textContent = String(text || ''); this._winBox.style.display = 'block'; // Auto-hide через 6с clearTimeout(this._winTimer); this._winTimer = setTimeout(() => { if (this._winBox) this._winBox.style.display = 'none'; }, 6000); } dispose() { try { this._root?.remove(); } catch (_) {} clearInterval(this._tickInterval); clearTimeout(this._msgTimer); clearTimeout(this._winTimer); this._root = null; } }