Files
bs-overlay/index.html
T
Yury 5ad61e7196 Add files via upload
fix download beatleader data
2026-03-16 20:38:46 +05:00

980 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>BS+ Cyberpunk Overlay</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
:root {
--neon-cyan: #0ff;
--neon-magenta: #f0f;
--neon-lime: #39ff14;
--neon-red: #ff3b3b;
--bg-glass: rgba(10, 10, 15, 0.65);
--border-glow: 0 0 10px rgba(0, 255, 255, 0.3);
}
body {
margin: 0;
padding: 0;
background: transparent;
font-family: 'Orbitron', sans-serif;
color: #fff;
overflow: hidden;
width: 100vw;
height: 100vh;
}
/* === АНИМАЦИИ === */
@keyframes rainbow {
0% { filter: hue-rotate(0deg); }
100% { filter: hue-rotate(360deg); }
}
#app-container {
position: absolute;
transition: transform 0.3s ease;
display: flex; /* Скрывается/показывается через JS */
flex-direction: column;
gap: 10px;
width: max-content;
max-width: 90vw;
}
.view-mode {
position: absolute;
transition: opacity 0.4s ease, transform 0.4s ease;
opacity: 0;
pointer-events: none;
display: flex;
flex-direction: column;
gap: 8px;
width: max-content;
}
.view-mode.active {
opacity: 1;
transform: scale(1);
}
.glass-panel {
background: var(--bg-glass);
backdrop-filter: blur(6px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 12px;
box-shadow: var(--border-glow);
display: flex;
flex-direction: column;
gap: 12px;
}
#header-row {
display: flex;
align-items: flex-start;
gap: 15px;
}
/* === КОНТЕЙНЕРЫ ДЛЯ ИЗОБРАЖЕНИЙ === */
#cover-wrapper, #bl-avatar-wrapper {
position: relative;
flex-shrink: 0;
}
#cover-wrapper { width: 90px; height: 90px; }
#bl-avatar-wrapper { width: 75px; height: 75px; display: none; }
#cover {
width: 100%; height: 100%;
border-radius: 10px;
object-fit: cover;
display: block;
}
#bl-avatar {
width: 100%; height: 100%;
border-radius: 12px;
object-fit: cover;
display: block;
}
.avatar-glow-wrapper::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
box-sizing: border-box;
}
#cover-wrapper::after { border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); }
#bl-avatar-wrapper::after { border-radius: 12px; border: 2px solid rgba(255,255,255,0.2); }
.avatar-glow-wrapper.active-glow::after {
box-shadow: 0 0 10px var(--neon-cyan);
border-color: var(--neon-cyan);
animation: rainbow 10s linear infinite;
}
#text-block {
display: flex;
flex-direction: column;
gap: 4px;
width: 320px;
}
#title {
font-size: 22px;
font-weight: 900;
background: linear-gradient(45deg, var(--neon-cyan), #fff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
white-space: normal;
word-wrap: break-word;
line-height: 1.2;
filter: drop-shadow(0 0 5px rgba(0,255,255,0.4));
}
#artist-mapper {
font-size: 14px;
color: #ccc;
letter-spacing: 0.5px;
white-space: normal;
word-wrap: break-word;
}
#meta-line {
font-size: 13px;
color: #d1d5db;
margin-top: 2px;
}
#meta-line span { margin-right: 12px; }
#difficulty { font-weight: bold; }
#bsr-line {
margin-top: 4px;
display: flex;
gap: 15px;
}
#key, #map-date {
font-size: 15px;
color: #d1d5db;
text-shadow: 1px 1px 2px #000;
}
/* === ШИРОКИЙ PROGRESS BAR === */
#stats-row {
display: flex;
padding-top: 5px;
border-top: 1px dashed rgba(255,255,255,0.1);
margin-top: 2px;
width: 100%;
}
#progress-wrapper {
position: relative;
width: 100%;
height: 18px;
background: rgba(0,0,0,0.6);
border-radius: 4px;
border: 1px solid rgba(0, 255, 255, 0.4);
box-shadow: 0 0 8px rgba(0, 255, 255, 0.3);
overflow: hidden;
display: flex;
}
#progress-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, var(--neon-magenta), var(--neon-cyan));
transition: width 0.1s linear;
}
#time-prog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: bold;
color: #fff;
text-shadow: 1px 1px 2px #000, -1px -1px 2px #000;
pointer-events: none;
}
#hp-bar-wrapper {
position: relative;
width: 100%;
height: 12px;
background: rgba(0,0,0,0.6);
border-radius: 6px;
overflow: hidden;
border: 1px solid rgba(255, 0, 255, 0.4);
box-shadow: 0 0 10px rgba(255, 0, 255, 0.2);
display: flex;
flex-shrink: 0;
}
#hp-bar-fill {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #8b0000, var(--neon-magenta));
transition: width 0.1s linear;
}
#hp-val {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
font-weight: bold;
text-shadow: 1px 1px 2px #000, -1px -1px 2px #000;
pointer-events: none;
}
#bottom-stats {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 5px;
width: 100%;
}
.bottom-stat-row {
display: flex;
gap: 20px;
}
.stat-item {
display: flex;
align-items: baseline;
gap: 8px;
}
.stat-item .label {
font-size: 11px;
color: #d1d5db;
text-transform: uppercase;
letter-spacing: 1px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
}
.stat-item .val {
font-size: 20px;
font-weight: 700;
text-shadow: 1px 1px 3px #000;
}
#combo-val { color: var(--neon-cyan); text-shadow: 0 0 8px rgba(0, 255, 255, 0.6), 1px 1px 2px #000; }
#miss-val { color: var(--neon-red); text-shadow: 0 0 8px rgba(255, 59, 59, 0.6), 1px 1px 2px #000; }
.stat-item.acc-large .label { font-size: 14px; }
.stat-item.acc-large #acc-num { font-size: 32px; color: #fff; }
.stat-item.acc-large #acc-grade { font-size: 32px; }
#bl-wrapper {
display: flex;
align-items: center;
gap: 15px;
}
#bl-info {
display: flex;
flex-direction: column;
justify-content: center;
gap: 3px;
}
#bl-name {
font-size: 18px;
font-weight: 900;
color: var(--neon-magenta);
text-shadow: 0 0 5px rgba(255,0,255,0.5);
margin-bottom: 2px;
}
.bl-stat-compact {
font-size: 13px;
color: #ccc;
}
.bl-stat-compact span {
color: var(--neon-cyan);
font-weight: bold;
text-shadow: 0 0 5px rgba(0,255,255,0.5);
}
/* === МЕНЮ НАСТРОЕК === */
#settings-modal {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%) scale(0.9);
background: rgba(10, 10, 15, 0.95);
border: 1px solid var(--neon-cyan);
box-shadow: 0 0 30px rgba(0, 255, 255, 0.3);
padding: 25px;
border-radius: 12px;
z-index: 999;
display: none;
flex-direction: column;
gap: 15px;
width: 360px;
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
}
#settings-modal.show { display: flex; opacity: 1; transform: translate(-50%, -50%) scale(1); }
#settings-modal h2 { margin: 0; color: var(--neon-cyan); text-align: center; font-size: 18px; }
.setting-row { display: flex; flex-direction: column; gap: 5px; }
.setting-row input[type="text"], .setting-row input[type="number"] {
background: rgba(0,0,0,0.5); color: #fff; border: 1px solid #444;
padding: 8px; font-family: 'Orbitron', sans-serif; border-radius: 4px; outline: none;
}
.setting-row input:focus { border-color: var(--neon-cyan); }
.radio-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.radio-label {
display: flex;
align-items: center;
gap: 6px;
background: rgba(0,0,0,0.5);
padding: 6px 8px;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: 0.2s;
}
.radio-label:hover { border-color: var(--neon-cyan); }
.radio-label input { margin: 0; cursor: pointer; accent-color: var(--neon-cyan); }
.modules-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 11px;
background: rgba(0,0,0,0.4);
padding: 6px;
border-radius: 4px;
border: 1px solid #333;
transition: 0.2s;
}
.checkbox-row:hover { border-color: var(--neon-cyan); }
.checkbox-row input { margin: 0; cursor: pointer; accent-color: var(--neon-cyan); }
.setting-row button {
background: transparent; color: var(--neon-cyan); border: 1px solid var(--neon-cyan); padding: 10px;
font-weight: bold; cursor: pointer; border-radius: 4px; margin-top: 5px;
font-family: 'Orbitron', sans-serif; transition: 0.2s;
}
.setting-row button:hover { background: var(--neon-cyan); color: #000; box-shadow: 0 0 15px var(--neon-cyan); }
#debug {
position: fixed; bottom: 10px; left: 10px; background: rgba(0,0,0,0.8);
color: var(--neon-lime); padding: 4px 8px; font-size: 11px;
border-radius: 4px; opacity: 0; transition: opacity 0.5s; z-index: 1000;
}
</style>
</head>
<body>
<div id="app-container">
<div id="menu-overlay" class="view-mode glass-panel">
<div id="bl-wrapper">
<div id="bl-avatar-wrapper" class="avatar-glow-wrapper active-glow">
<img id="bl-avatar" src="" alt="Avatar">
</div>
<div id="bl-info">
<div id="bl-name">Loading...</div>
<div class="bl-stat-compact">Global: <span id="bl-global">#--</span></div>
<div class="bl-stat-compact">Local: <span id="bl-local">#--</span></div>
<div class="bl-stat-compact">PP: <span id="bl-pp">-- pp</span></div>
</div>
</div>
</div>
<div id="playing-overlay" class="view-mode">
<div class="glass-panel" id="top-glass-panel">
<div id="header-row">
<div id="cover-wrapper" class="avatar-glow-wrapper active-glow">
<img id="cover" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" alt="Cover">
</div>
<div id="text-block">
<div id="title">Waiting for song...</div>
<div id="artist-mapper">-</div>
<div id="meta-line">
<span id="difficulty">-</span>
<span id="bpm">BPM -</span>
</div>
<div id="bsr-line">
<span id="key">BSR: -</span>
<span id="map-date"></span>
</div>
</div>
</div>
<div id="stats-row">
<div id="progress-wrapper">
<div id="progress-fill"></div>
<div id="time-prog">0:00 / 0:00</div>
</div>
</div>
</div>
<div id="hp-bar-wrapper">
<div id="hp-bar-fill"></div>
<div id="hp-val">100%</div>
</div>
<div id="bottom-stats">
<div class="bottom-stat-row">
<div class="stat-item"><span class="label">Miss</span><span class="val" id="miss-val">0</span></div>
<div class="stat-item"><span class="label">Combo</span><span class="val" id="combo-val">0</span></div>
</div>
<div class="stat-item acc-large">
<span class="label">Acc</span>
<span class="val" id="acc-num">0.0%</span>
<span class="val" id="acc-grade" style="color:var(--neon-cyan); text-shadow: 0 0 10px var(--neon-cyan)">SS</span>
</div>
</div>
</div>
</div>
<div id="settings-modal">
<h2>SYSTEM SETUP [F2]</h2>
<div class="setting-row">
<label style="font-size: 12px; color: #ccc;">WebSocket URL</label>
<input type="text" id="inp-ws" placeholder="ws://127.0.0.1:2947/socket">
</div>
<div class="setting-row">
<label style="font-size: 12px; color: #ccc;">Layout (Выравнивание)</label>
<div class="radio-grid">
<label class="radio-label"><input type="radio" name="layout" value="top-left"> Top Left</label>
<label class="radio-label"><input type="radio" name="layout" value="top-right"> Top Right</label>
<label class="radio-label"><input type="radio" name="layout" value="bottom-left"> Bottom Left</label>
<label class="radio-label"><input type="radio" name="layout" value="bottom-right"> Bottom Right</label>
</div>
</div>
<div class="setting-row">
<label style="font-size: 12px; color: #ccc;">Scale (0.5 - 2.0)</label>
<input type="number" step="0.1" id="inp-scale">
</div>
<div class="setting-row">
<label style="font-size: 12px; color: #ccc;">BeatLeader ID / Nickname</label>
<input type="text" id="inp-bl" placeholder="Например: 76561198029377687">
</div>
<div class="setting-row">
<label style="font-size: 12px; color: #ccc; margin-top: 5px;">Модули отображения (Вкл/Выкл)</label>
<div class="modules-grid">
<label class="checkbox-row"><input type="checkbox" id="inp-show-bl"> BeatLeader Меню</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-cover"> Обложка трека</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-title"> Название трека</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-artist"> Исполнитель / Маппер</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-meta"> Сложность и BPM</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-bsr"> BSR код и Дата</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-progress"> Прогресс-бар</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-hp"> HP Бар (Здоровье)</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-stats"> Miss / Combo</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-acc"> Точность (Acc)</label>
<label class="checkbox-row"><input type="checkbox" id="inp-glow-avatar"> Неоновое свечение</label>
<label class="checkbox-row"><input type="checkbox" id="inp-show-debug"> Debug сообщения</label>
</div>
</div>
<div class="setting-row">
<button onclick="saveSettings()">APPLY & RECONNECT</button>
</div>
</div>
<div id="debug">System init...</div>
<script>
const els = {
app: document.getElementById('app-container'),
menuOverlay: document.getElementById('menu-overlay'),
playingOverlay: document.getElementById('playing-overlay'),
topGlassPanel: document.getElementById('top-glass-panel'),
headerRow: document.getElementById('header-row'),
textBlock: document.getElementById('text-block'),
statsRow: document.getElementById('stats-row'),
progFill: document.getElementById('progress-fill'),
time: document.getElementById('time-prog'),
title: document.getElementById('title'),
artist: document.getElementById('artist-mapper'),
metaLine: document.getElementById('meta-line'),
bsrLine: document.getElementById('bsr-line'),
diff: document.getElementById('difficulty'),
bpm: document.getElementById('bpm'),
key: document.getElementById('key'),
date: document.getElementById('map-date'),
coverWrapper: document.getElementById('cover-wrapper'),
cover: document.getElementById('cover'),
bottomStats: document.getElementById('bottom-stats'),
bottomStatRow: document.querySelector('.bottom-stat-row'),
accLarge: document.querySelector('.acc-large'),
accNum: document.getElementById('acc-num'),
accGrade: document.getElementById('acc-grade'),
combo: document.getElementById('combo-val'),
miss: document.getElementById('miss-val'),
hpBarWrapper: document.getElementById('hp-bar-wrapper'),
hpVal: document.getElementById('hp-val'),
hpFill: document.getElementById('hp-bar-fill'),
debug: document.getElementById('debug'),
settings: document.getElementById('settings-modal'),
blAvatarWrapper: document.getElementById('bl-avatar-wrapper'),
blAvatar: document.getElementById('bl-avatar'),
blName: document.getElementById('bl-name'),
blGlobal: document.getElementById('bl-global'),
blLocal: document.getElementById('bl-local'),
blPp: document.getElementById('bl-pp')
};
let config = {
ws: 'ws://127.0.0.1:2947/socket',
layout: 'top-left',
scale: 1.0,
blId: '',
showBL: true,
showDebugUI: true,
glowAvatar: true,
showCover: true,
showTitle: true,
showArtist: true,
showMeta: true,
showBsr: true,
showProgress: true,
showHp: true,
showStats: true,
showAcc: true
};
let ws = null;
let reconnectAttempts = 0;
let reconnectTimeout = null;
let debugTimeout = null;
let duration = 0;
// Переменные для BeatLeader
let lastBlFetch = 0;
let isFetchingBL = false;
let currentProxyIdx = 0;
// Массив прокси (первый элемент пустой - попытка загрузки напрямую, т.к. API BL поддерживает CORS)
const proxies = [
"",
"https://api.codetabs.com/v1/proxy?quest=",
"https://api.allorigins.win/raw?url=",
"https://corsproxy.io/?",
"https://thingproxy.freeboard.io/fetch/"
];
function init() {
els.app.style.display = 'none';
loadSettings();
applyLayout();
applyModules();
applyGlow();
connectWS();
// Фоновое обновление профиля каждые 15 минут
setInterval(() => fetchBL(), 900000);
document.addEventListener('keydown', (e) => {
if (e.key === 'F2') els.settings.classList.toggle('show');
});
}
function loadSettings() {
const saved = localStorage.getItem('bsCyberConfig');
if (saved) config = { ...config, ...JSON.parse(saved) };
document.getElementById('inp-ws').value = config.ws;
document.getElementById('inp-scale').value = config.scale;
document.getElementById('inp-bl').value = config.blId;
document.getElementById('inp-show-bl').checked = config.showBL !== false;
document.getElementById('inp-show-debug').checked = config.showDebugUI !== false;
document.getElementById('inp-glow-avatar').checked = config.glowAvatar !== false;
document.getElementById('inp-show-cover').checked = config.showCover !== false;
document.getElementById('inp-show-title').checked = config.showTitle !== false;
document.getElementById('inp-show-artist').checked = config.showArtist !== false;
document.getElementById('inp-show-meta').checked = config.showMeta !== false;
document.getElementById('inp-show-bsr').checked = config.showBsr !== false;
document.getElementById('inp-show-progress').checked = config.showProgress !== false;
document.getElementById('inp-show-hp').checked = config.showHp !== false;
document.getElementById('inp-show-stats').checked = config.showStats !== false;
document.getElementById('inp-show-acc').checked = config.showAcc !== false;
const radio = document.querySelector(`input[name="layout"][value="${config.layout}"]`);
if(radio) radio.checked = true;
}
function saveSettings() {
config.ws = document.getElementById('inp-ws').value;
config.scale = parseFloat(document.getElementById('inp-scale').value) || 1.0;
config.blId = document.getElementById('inp-bl').value.trim();
config.showBL = document.getElementById('inp-show-bl').checked;
config.showDebugUI = document.getElementById('inp-show-debug').checked;
config.glowAvatar = document.getElementById('inp-glow-avatar').checked;
config.showCover = document.getElementById('inp-show-cover').checked;
config.showTitle = document.getElementById('inp-show-title').checked;
config.showArtist = document.getElementById('inp-show-artist').checked;
config.showMeta = document.getElementById('inp-show-meta').checked;
config.showBsr = document.getElementById('inp-show-bsr').checked;
config.showProgress = document.getElementById('inp-show-progress').checked;
config.showHp = document.getElementById('inp-show-hp').checked;
config.showStats = document.getElementById('inp-show-stats').checked;
config.showAcc = document.getElementById('inp-show-acc').checked;
const checkedRadio = document.querySelector('input[name="layout"]:checked');
if(checkedRadio) config.layout = checkedRadio.value;
localStorage.setItem('bsCyberConfig', JSON.stringify(config));
els.settings.classList.remove('show');
applyLayout();
applyModules();
applyGlow();
connectWS();
}
function applyLayout() {
const isLeft = config.layout.includes('left');
const isTop = config.layout.includes('top');
els.app.style.transformOrigin = `${isLeft ? 'left' : 'right'} ${isTop ? 'top' : 'bottom'}`;
els.app.style.transform = `scale(${config.scale})`;
els.app.style.top = isTop ? '20px' : 'auto';
els.app.style.bottom = isTop ? 'auto' : '20px';
els.app.style.left = isLeft ? '20px' : 'auto';
els.app.style.right = isLeft ? 'auto' : '20px';
els.playingOverlay.style.flexDirection = isTop ? 'column' : 'column-reverse';
els.menuOverlay.style.top = isTop ? '0' : 'auto';
els.menuOverlay.style.bottom = isTop ? 'auto' : '0';
els.playingOverlay.style.top = isTop ? '0' : 'auto';
els.playingOverlay.style.bottom = isTop ? 'auto' : '0';
els.app.style.alignItems = isLeft ? 'flex-start' : 'flex-end';
els.playingOverlay.style.alignItems = isLeft ? 'flex-start' : 'flex-end';
els.headerRow.style.flexDirection = isLeft ? 'row' : 'row-reverse';
els.textBlock.style.alignItems = isLeft ? 'flex-start' : 'flex-end';
els.textBlock.style.textAlign = isLeft ? 'left' : 'right';
els.statsRow.style.justifyContent = isLeft ? 'flex-start' : 'flex-end';
els.bottomStats.style.alignItems = isLeft ? 'flex-start' : 'flex-end';
els.bottomStatRow.style.flexDirection = isLeft ? 'row' : 'row-reverse';
document.getElementById('bl-wrapper').style.flexDirection = isLeft ? 'row' : 'row-reverse';
document.getElementById('bl-info').style.alignItems = isLeft ? 'flex-start' : 'flex-end';
document.getElementById('bl-info').style.textAlign = isLeft ? 'left' : 'right';
document.getElementById('bsr-line').style.justifyContent = isLeft ? 'flex-start' : 'flex-end';
els.hpFill.style.marginLeft = isLeft ? '0' : 'auto';
els.progFill.style.marginLeft = isLeft ? '0' : 'auto';
}
function applyModules() {
els.coverWrapper.style.display = config.showCover ? 'flex' : 'none';
els.title.style.display = config.showTitle ? '' : 'none';
els.artist.style.display = config.showArtist ? '' : 'none';
els.metaLine.style.display = config.showMeta ? '' : 'none';
els.bsrLine.style.display = config.showBsr ? '' : 'none';
const showAnyText = config.showTitle || config.showArtist || config.showMeta || config.showBsr;
els.textBlock.style.display = showAnyText ? 'flex' : 'none';
const showHeader = config.showCover || showAnyText;
els.headerRow.style.display = showHeader ? 'flex' : 'none';
els.statsRow.style.display = config.showProgress ? 'flex' : 'none';
const showTopPanel = showHeader || config.showProgress;
els.topGlassPanel.style.display = showTopPanel ? 'flex' : 'none';
els.hpBarWrapper.style.display = config.showHp ? 'flex' : 'none';
els.bottomStatRow.style.display = config.showStats ? 'flex' : 'none';
els.accLarge.style.display = config.showAcc ? 'flex' : 'none';
const showBottomStats = config.showStats || config.showAcc;
els.bottomStats.style.display = showBottomStats ? 'flex' : 'none';
}
function applyGlow() {
if(els.blAvatarWrapper) els.blAvatarWrapper.classList.toggle('active-glow', config.glowAvatar !== false);
if(els.coverWrapper) els.coverWrapper.classList.toggle('active-glow', config.glowAvatar !== false);
}
function showDebug(msg) {
console.log("[BS+ Overlay]", msg);
if (config.showDebugUI === false) return;
clearTimeout(debugTimeout);
els.debug.textContent = msg;
els.debug.style.opacity = 1;
debugTimeout = setTimeout(() => els.debug.style.opacity = 0, 5000);
}
function setMode(mode) {
if (mode === 'playing') {
els.menuOverlay.classList.remove('active');
els.playingOverlay.classList.add('active');
} else {
els.playingOverlay.classList.remove('active');
if (config.showBL) {
els.menuOverlay.classList.add('active');
fetchBL();
} else {
els.menuOverlay.classList.remove('active');
}
}
}
function getGrade(acc) {
let grade = 'E';
if (acc >= 0.9) grade = 'SS';
else if (acc >= 0.8) grade = 'S';
else if (acc >= 0.65) grade = 'A';
else if (acc >= 0.5) grade = 'B';
else if (acc >= 0.35) grade = 'C';
else if (acc >= 0.2) grade = 'D';
let color = '#e0e0e0';
if (acc >= 0.95) color = '#b046ff';
else if (acc >= 0.90) color = '#ff3b3b';
else if (acc >= 0.85) color = '#ff9800';
else if (acc >= 0.80) color = '#00e5ff';
else if (acc >= 0.70) color = '#39ff14';
return { grade, color };
}
function getDifficultyStyle(diff) {
const d = diff.toLowerCase();
if (d.includes('easy')) return { color: '#3cb371', shadow: 'rgba(60, 179, 113, 0.5)' };
if (d.includes('normal')) return { color: '#59b0f4', shadow: 'rgba(89, 176, 244, 0.5)' };
if (d.includes('hard')) return { color: '#ff9800', shadow: 'rgba(255, 152, 0, 0.5)' };
if (d.includes('expert+') || d.includes('expertplus')) return { color: '#8f48db', shadow: 'rgba(143, 72, 219, 0.5)' };
if (d.includes('expert')) return { color: '#e53935', shadow: 'rgba(229, 57, 53, 0.5)' };
return { color: '#fff', shadow: 'rgba(255, 255, 255, 0.5)' };
}
async function fetchBSR(hash) {
try {
els.key.textContent = `BSR: Loading...`;
els.date.textContent = ``;
const res = await fetch(`https://api.beatsaver.com/maps/hash/${hash}`);
if (!res.ok) throw new Error("Not found");
const data = await res.json();
els.key.textContent = `BSR: ${data.id}`;
if (data.uploaded) {
const date = new Date(data.uploaded);
els.date.textContent = date.toLocaleDateString();
}
} catch (err) {
els.key.textContent = `BSR: N/A`;
els.date.textContent = ``;
}
}
async function fetchBL(force = false) {
if (!config.blId || !config.showBL) return;
if (isFetchingBL) return;
if (!force && Date.now() - lastBlFetch < 900000) return;
isFetchingBL = true;
const isNumeric = /^\d+$/.test(config.blId);
const originalUrl = isNumeric
? `https://api.beatleader.com/player/${config.blId}?stats=true`
: `https://api.beatleader.com/players?search=${encodeURIComponent(config.blId)}`;
let success = false;
let attempts = 0;
const maxAttempts = 5;
while (!success && attempts < maxAttempts) {
try {
const proxy = proxies[currentProxyIdx % proxies.length];
const targetUrl = proxy ? proxy + encodeURIComponent(originalUrl) : originalUrl;
showDebug(`BL Fetch [${attempts + 1}/${maxAttempts}] via ${proxy ? 'Proxy' : 'Direct'}`);
const res = await fetch(targetUrl, { headers: { 'Accept': 'application/json' } });
if (!res.ok) throw new Error(`Network error: ${res.status}`);
const json = await res.json();
const player = json.data ? json.data[0] : json;
if (!player || !player.name) throw new Error("Player not found");
els.blName.textContent = player.name || "Unknown";
els.blGlobal.textContent = player.rank ? `#${player.rank.toLocaleString()}` : "#--";
els.blLocal.textContent = player.countryRank ? `#${player.countryRank.toLocaleString()} (${player.country || 'N/A'})` : "#-- (N/A)";
els.blPp.textContent = player.pp ? `${Math.round(player.pp).toLocaleString()}` : "--";
if (player.avatar) {
els.blAvatar.src = player.avatar;
els.blAvatarWrapper.style.display = 'block';
} else {
els.blAvatarWrapper.style.display = 'none';
}
lastBlFetch = Date.now();
success = true;
showDebug(`BL Profile Loaded Successfully!`);
} catch (err) {
attempts++;
currentProxyIdx++; // Пробуем следующий прокси
showDebug(`BL Error: ${err.message}. Retrying...`);
if (attempts < maxAttempts) {
await new Promise(r => setTimeout(r, 2000)); // Задержка 2 сек перед следующей попыткой
}
}
}
if (!success) {
els.blName.textContent = "Error loading profile";
showDebug(`BL Fetch failed completely after ${maxAttempts} attempts.`);
}
isFetchingBL = false;
}
function connectWS() {
if (ws) { ws.onclose = null; ws.close(); }
clearTimeout(reconnectTimeout);
els.app.style.display = 'none';
showDebug(`Connecting to ${config.ws}...`);
try { ws = new WebSocket(config.ws); } catch (e) { handleDisconnect(); return; }
ws.onopen = () => {
showDebug('✅ WebSocket Connected');
reconnectAttempts = 0;
els.app.style.display = 'flex';
setMode('menu');
};
ws.onclose = () => handleDisconnect();
ws.onerror = () => handleDisconnect();
ws.onmessage = (ev) => {
const data = JSON.parse(ev.data);
const event = data._event;
if (event === 'gameState') {
if (data.gameStateChanged === 'Playing') setMode('playing');
else setMode('menu');
}
if (event === 'mapInfo' && data.mapInfoChanged) {
const m = data.mapInfoChanged;
els.title.textContent = m.sub_name ? `${m.name} ${m.sub_name}` : m.name;
const mapper = (m.mapper && m.mapper !== 'Not mapped') ? m.mapper : '';
els.artist.textContent = mapper ? `${m.artist} // ${mapper}` : m.artist;
els.diff.textContent = `${m.characteristic} ${m.difficulty}`;
const diffStyle = getDifficultyStyle(m.difficulty);
els.diff.style.color = diffStyle.color;
els.diff.style.textShadow = `0 0 5px ${diffStyle.shadow}`;
els.bpm.textContent = `BPM ${Math.round(m.BPM || 0)}`;
if (m.coverRaw) els.cover.src = `data:image/png;base64,${m.coverRaw}`;
duration = (m.duration || 0) / 1000;
if (m.BSRKey) {
els.key.textContent = `BSR: ${m.BSRKey}`;
els.date.textContent = ``;
if (m.level_id?.startsWith('custom_level_')) fetchBSR(m.level_id.substring(13));
} else if (m.level_id?.startsWith('custom_level_')) {
fetchBSR(m.level_id.substring(13));
} else {
els.key.textContent = `OST/DLC`;
els.date.textContent = ``;
}
}
if (event === 'score' && data.scoreEvent) {
const s = data.scoreEvent;
if (s.accuracy !== undefined) {
const accRaw = s.accuracy;
const accObj = getGrade(accRaw);
els.accNum.textContent = `${(accRaw * 100).toFixed(1)}%`;
els.accGrade.textContent = accObj.grade;
els.accGrade.style.color = accObj.color;
els.accGrade.style.textShadow = `0 0 10px ${accObj.color}, 1px 1px 3px #000`;
}
if (s.combo !== undefined) els.combo.textContent = s.combo;
if (s.missCount !== undefined) els.miss.textContent = s.missCount;
if (s.currentHealth !== undefined) {
const hpPct = Math.round(s.currentHealth * 100);
els.hpVal.textContent = `${hpPct}%`;
els.hpFill.style.width = `${hpPct}%`;
}
if (duration > 0 && s.time !== undefined) {
const pct = Math.min((s.time / duration) * 100, 100);
els.progFill.style.width = `${pct}%`;
const curM = Math.floor(s.time / 60);
const curS = Math.floor(s.time % 60).toString().padStart(2, '0');
const totM = Math.floor(duration / 60);
const totS = Math.floor(duration % 60).toString().padStart(2, '0');
els.time.textContent = `${curM}:${curS} / ${totM}:${totS}`;
}
}
};
}
function handleDisconnect() {
els.app.style.display = 'none';
if (reconnectAttempts < 10) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
showDebug(`❌ WS Lost. Reconnecting in ${delay/1000}s...`);
reconnectAttempts++;
reconnectTimeout = setTimeout(connectWS, delay);
} else {
showDebug(`❌ Max reconnects reached. F2 to reconfigure.`);
}
}
init();
</script>
</body>
</html>