GG / GG.html
Mycc's picture
Редизайн
79b7bff verified
Raw History Blame Contribute Delete
102 kB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Рулетка Genshin Impact</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Unicase:wght@500;600;700&family=Cormorant:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body{
--red:#C41E2F; --red-bright:#FF4655; --red-dark:#6E0F1A; --blood:#8B0000;
--line:rgba(196,30,47,.45); --line-soft:rgba(196,30,47,.22);
font-family:'Inter',sans-serif; transition:background-color .3s,color .3s;
}
body.dark{ --bg:#08080D; --panel:rgba(20,20,30,.85); --panel2:rgba(27,27,40,.9); --bone:#E8E2DA; --dim:#9A96A3; background:var(--bg); color:var(--bone); }
body.light{ --bg:#E9E4DC; --panel:rgba(247,242,234,.9); --panel2:rgba(239,232,222,.95); --bone:#221C18; --dim:#6E675F; background:var(--bg); color:var(--bone); }
h1,h2,h3,.gothic{ font-family:'Cormorant Unicase','Cormorant',serif; letter-spacing:.06em; }
/* ===== ФОНОВЫЕ СЛОИ ===== */
.rv-bg{ position:fixed; inset:0; z-index:0; overflow:hidden; pointer-events:none; }
.rv-vignette{ position:absolute; inset:0;
background:
radial-gradient(1100px 560px at 50% -12%, rgba(196,30,47,.20), transparent 62%),
radial-gradient(900px 520px at 88% 112%, rgba(110,15,26,.28), transparent 60%),
radial-gradient(700px 420px at 6% 108%, rgba(139,0,0,.18), transparent 60%);
}
.rv-pattern{ position:absolute; inset:-120px; will-change:transform;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Cpath d='M32 10l5 17 17 5-17 5-5 17-5-17-17-5 17-5z' fill='none' stroke='%23C41E2F' stroke-opacity='.13'/%3E%3Ccircle cx='32' cy='32' r='1.6' fill='%23C41E2F' fill-opacity='.12'/%3E%3C/svg%3E");
}
.rv-eye-wrap{ position:absolute; top:-130px; left:0; right:0; display:flex; justify-content:center; }
.rv-eye{ width:min(780px,92vw); color:rgba(196,30,47,.11); will-change:transform; transition:transform .25s ease-out; }
body.light .rv-eye{ color:rgba(139,0,0,.10); }
.rv-fx{ position:absolute; inset:0; }
/* Canvas для пыли */
#dust-canvas{ position:fixed; inset:0; z-index:1; pointer-events:none; }
/* ===== АНИМАЦИИ ГЛАЗ ===== */
.rv-eye-instance{
position:absolute;
width:60px; height:30px;
opacity:0;
pointer-events:none;
animation: eyeAppear 8s ease-in-out forwards;
}
.rv-eye-instance svg{ width:100%; height:100%; }
@keyframes eyeAppear{
0%{ opacity:0; transform:scale(0.3) scaleY(0); }
8%{ opacity:0.9; transform:scale(1) scaleY(1); }
25%{ opacity:0.9; transform:scale(1) scaleY(1); }
28%{ opacity:0.9; transform:scale(1) scaleY(0.1); }
31%{ opacity:0.9; transform:scale(1) scaleY(1); }
65%{ opacity:0.9; transform:scale(1) scaleY(1); }
68%{ opacity:0.9; transform:scale(1) scaleY(0.1); }
71%{ opacity:0.9; transform:scale(1) scaleY(1); }
92%{ opacity:0.7; transform:scale(1) scaleY(1); }
100%{ opacity:0; transform:scale(0.8) scaleY(0); }
}
/* ===== КРЕСТЫ ===== */
.fx-cross{ position:absolute; color:var(--red-bright); font-family:'Cormorant Unicase',serif; font-weight:700;
opacity:0; pointer-events:none; text-shadow:0 0 10px rgba(255,70,85,.8),0 0 32px rgba(196,30,47,.5);
animation:crossFade ease-in-out forwards; }
@keyframes crossFade{ 0%{opacity:0;transform:scale(.5) rotate(-10deg);} 25%{opacity:.9;} 55%{opacity:.5;} 100%{opacity:0;transform:scale(1.2) rotate(8deg);} }
/* ===== УГОЛКИ ===== */
.cornered{ position:relative; }
.cornered::before{ content:""; position:absolute; inset:0; pointer-events:none; z-index:1;
background-image:
linear-gradient(var(--red),var(--red)),linear-gradient(var(--red),var(--red)),
linear-gradient(var(--red),var(--red)),linear-gradient(var(--red),var(--red)),
linear-gradient(var(--red),var(--red)),linear-gradient(var(--red),var(--red)),
linear-gradient(var(--red),var(--red)),linear-gradient(var(--red),var(--red));
background-repeat:no-repeat;
background-position:top left,top left,top right,top right,bottom left,bottom left,bottom right,bottom right;
background-size:18px 2px,2px 18px,18px 2px,2px 18px,18px 2px,2px 18px,18px 2px,2px 18px; }
.ornament{ display:flex; align-items:center; gap:14px; max-width:440px; margin:0 auto 18px; color:var(--red-bright); font-family:'Cormorant Unicase',serif; }
.ornament::before,.ornament::after{ content:""; flex:1; height:1px; }
.ornament::before{ background:linear-gradient(90deg,transparent,var(--red)); }
.ornament::after{ background:linear-gradient(90deg,var(--red),transparent); }
/* ===== ПАНЕЛИ ===== */
.section-panel{ background:var(--panel); backdrop-filter:blur(12px); border:1px solid var(--line-soft); border-radius:16px; box-shadow:0 12px 32px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.05); }
body.light .section-panel{ box-shadow:0 12px 32px rgba(139,0,0,.12), inset 0 1px 0 rgba(255,255,255,.5); }
.result-panel{ background:var(--panel2); backdrop-filter:blur(8px); border:1px solid var(--line-soft); border-radius:14px; }
.rv-title{ color:var(--bone); text-shadow:0 0 26px rgba(196,30,47,.55),0 2px 4px rgba(0,0,0,.4); }
.rv-sub{ color:var(--dim); }
/* ===== КНОПКИ ===== */
.rv-btn{ font-family:'Cormorant Unicase',serif; text-transform:uppercase; letter-spacing:.14em; font-weight:700;
background:linear-gradient(180deg,var(--panel2),var(--panel)); color:var(--bone);
border:1px solid var(--line); transition:all .25s ease; position:relative; overflow:hidden; }
.rv-btn::before{ content:''; position:absolute; inset:0; background:linear-gradient(180deg,rgba(255,70,85,.1),transparent); opacity:0; transition:opacity .25s; }
.rv-btn:hover::before{ opacity:1; }
.rv-btn:hover{ border-color:var(--red-bright); color:var(--red-bright); box-shadow:0 0 16px rgba(255,70,85,.25); transform:translateY(-2px); }
.rv-btn:active{ transform:translateY(0); }
.rv-btn-primary{ background:linear-gradient(180deg,var(--red),var(--red-dark)); color:#FFF5F5; border-color:rgba(255,70,85,.6); box-shadow:0 4px 18px rgba(196,30,47,.4); }
.rv-btn-primary:hover{ color:#fff; box-shadow:0 6px 28px rgba(255,70,85,.55); }
.rv-btn-danger{ background:linear-gradient(180deg,#3A0B12,#24070C); color:#FFB9C0; border-color:rgba(196,30,47,.55); }
.rv-btn-danger:hover{ color:#FF4655; box-shadow:0 0 18px rgba(255,70,85,.35); }
.rv-iconbtn{ border:1px solid var(--line); background:var(--panel); color:var(--red-bright); transition:all .25s; backdrop-filter:blur(8px); }
.rv-iconbtn:hover{ box-shadow:0 0 12px rgba(255,70,85,.35); border-color:var(--red-bright); transform:rotate(12deg); }
.rv-input{ background:var(--panel); backdrop-filter:blur(8px); border:1px solid var(--line); border-radius:10px; padding:10px 14px; color:var(--bone); font-weight:500; transition:all .25s; }
.rv-input:focus{ outline:none; border-color:var(--red-bright); box-shadow:0 0 12px rgba(255,70,85,.25); }
select.rv-input option{ background:var(--panel); color:var(--bone); }
.rv-label{ color:var(--red-bright); font-family:'Cormorant Unicase',serif; letter-spacing:.08em; font-weight:700; }
/* ===== КАРТОЧКИ ===== */
.char-slot{ display:flex; flex-direction:column; align-items:center; flex-shrink:0; width:110px; }
.char-card{ display:flex; flex-direction:column; align-items:center; padding:10px; border-radius:12px;
background:var(--panel); backdrop-filter:blur(8px); border:1px solid var(--line-soft); text-align:center; flex-shrink:0; width:110px; transition:all .25s; }
@media(min-width:640px){ .char-slot,.char-card{ width:130px; } }
@media(min-width:768px){ .char-slot,.char-card{ width:140px; } }
@media(min-width:1024px){ .char-slot,.char-card{ width:150px; } }
.char-card:hover{ border-color:var(--red-bright); box-shadow:0 0 16px rgba(255,70,85,.25); transform:translateY(-2px); }
.char-card img{ width:80px; height:80px; border-radius:50%; object-fit:cover; border:2px solid var(--red); box-shadow:0 0 10px rgba(196,30,47,.35); margin-bottom:6px; }
@media(min-width:640px){ .char-card img{ width:96px; height:96px; } }
.char-card p{ font-size:.85rem; font-weight:600; color:var(--bone); width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.player-char-card{ position:relative; cursor:pointer; border:1px solid var(--line-soft); border-radius:12px;
background:var(--panel); backdrop-filter:blur(8px); transition:all .25s; }
.player-char-card.selected{ border-color:var(--red-bright); box-shadow:0 0 14px rgba(255,70,85,.35); }
.player-char-card:hover{ border-color:var(--red-bright); transform:translateY(-2px); }
.player-char-card .remove-btn{ position:absolute; top:-7px; right:-7px; background:linear-gradient(135deg,#DC2626,#7A0E1B); color:#fff;
border-radius:50%; width:20px; height:20px; display:flex; justify-content:center; align-items:center;
font-size:.85rem; font-weight:bold; cursor:pointer; opacity:0; transition:opacity .2s; box-shadow:0 2px 8px rgba(0,0,0,.4); }
.player-char-card:hover .remove-btn{ opacity:1; }
.boss-card{ flex:1; min-width:220px; max-width:280px; text-align:center; padding:16px; border-radius:14px;
background:var(--panel); backdrop-filter:blur(8px); box-shadow:0 4px 14px rgba(0,0,0,.25); transition:all .25s; border:1px solid var(--line-soft); cursor:pointer; }
.boss-card:hover{ transform:translateY(-3px); border-color:var(--red-bright); box-shadow:0 8px 22px rgba(255,70,85,.25); }
.boss-card img{ width:90px; height:90px; border-radius:50%; object-fit:cover; border:2px solid var(--red); margin-bottom:10px; box-shadow:0 0 12px rgba(196,30,47,.35); }
.boss-card h3{ font-weight:700; margin-bottom:6px; font-size:1.1rem; color:var(--bone); }
.boss-card p{ font-size:.9rem; color:var(--dim); margin-top:4px; }
.legend-card{ border-color:var(--line); }
.full-history-character-card{ display:flex; flex-direction:column; align-items:center; padding:10px; border-radius:12px;
background:var(--panel); backdrop-filter:blur(8px); box-shadow:0 2px 8px rgba(0,0,0,.2); text-align:center; flex-shrink:0; width:110px; border:1px solid var(--line); }
.full-history-character-card img{ width:64px; height:64px; border-radius:50%; margin-bottom:6px; object-fit:cover; border:2px solid var(--red); }
.full-history-character-card p{ font-size:.875rem; font-weight:600; text-overflow:ellipsis; overflow:hidden; white-space:nowrap; width:100%; color:var(--bone); }
.history-item{ border-left:3px solid var(--red); padding-left:16px; }
.boss-history-item{ border-left:3px solid var(--red-dark); padding-left:16px; }
.category-select,.legend-settings{ background:var(--panel2); backdrop-filter:blur(8px); border:1px solid var(--line-soft); border-radius:14px; padding:16px; margin-bottom:24px; }
.boss-toggle{ margin-bottom:8px; padding:10px; border-radius:10px; transition:all .2s; border:1px solid var(--line-soft); }
.boss-toggle:hover{ background:rgba(196,30,47,.08); border-color:var(--line); }
.boss-toggle input[type="checkbox"]{ margin-right:10px; width:18px; height:18px; accent-color:var(--red); }
.modal-panel{ background:var(--panel); backdrop-filter:blur(16px); border:1px solid var(--line); border-radius:18px; box-shadow:0 16px 48px rgba(0,0,0,.6),0 0 40px rgba(196,30,47,.15); }
.loader{ width:48px; height:48px; border:3px solid rgba(196,30,47,.2); border-top-color:var(--red-bright); border-radius:50%;
display:inline-block; box-sizing:border-box; animation:rotation 1s linear infinite; }
@keyframes rotation{ 0%{transform:rotate(0deg);} 100%{transform:rotate(360deg);} }
.settings-panel{ max-height:0; overflow:hidden; transition:max-height .35s ease-out; }
.settings-panel.open{ max-height:2200px; }
.roulette-item-spin{ animation:spin-in-place .2s ease-in-out infinite alternate; }
@keyframes spin-in-place{ 0%{transform:scale(.9) rotate(-2deg);opacity:.7;} 100%{transform:scale(1.1) rotate(2deg);opacity:1;} }
.roulette-item-boom-enter{ opacity:0; transform:scale(.8); }
.roulette-item-boom-enter-active{ animation:boom-effect .45s cubic-bezier(.68,-.55,.27,1.55) forwards; }
@keyframes boom-effect{
0%{ opacity:0; transform:scale(.8); filter:brightness(1.6) drop-shadow(0 0 5px rgba(255,70,85,0)); }
50%{ opacity:1; transform:scale(1.08); filter:brightness(1.9) drop-shadow(0 0 22px rgba(255,70,85,.85)); }
100%{ opacity:1; transform:scale(1); filter:brightness(1) drop-shadow(0 0 10px rgba(196,30,47,.5)); } }
.custom-scrollbar::-webkit-scrollbar{ height:8px; width:8px; }
.custom-scrollbar::-webkit-scrollbar-track{ background:rgba(196,30,47,.08); border-radius:10px; }
.custom-scrollbar::-webkit-scrollbar-thumb{ background:var(--red); border-radius:10px; }
.custom-scrollbar::-webkit-scrollbar-thumb:hover{ background:var(--red-bright); }
/* ===== МУЗЫКАЛЬНЫЙ КОНТРОЛ ===== */
.music-control{ position:fixed; bottom:20px; right:20px; z-index:100; display:flex; align-items:center; gap:10px; }
.music-btn{ width:44px; height:44px; border-radius:50%; background:var(--panel); backdrop-filter:blur(12px); border:1px solid var(--line); color:var(--red-bright); display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all .25s; }
.music-btn:hover{ box-shadow:0 0 16px rgba(255,70,85,.4); border-color:var(--red-bright); transform:scale(1.05); }
.music-btn svg{ width:20px; height:20px; }
.volume-slider{ width:100px; height:6px; -webkit-appearance:none; appearance:none; background:rgba(196,30,47,.2); border-radius:3px; outline:none; opacity:0; transition:opacity .25s; }
.music-control:hover .volume-slider{ opacity:1; }
.volume-slider::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:14px; height:14px; border-radius:50%; background:var(--red-bright); cursor:pointer; box-shadow:0 0 8px rgba(255,70,85,.5); }
.volume-slider::-moz-range-thumb{ width:14px; height:14px; border-radius:50%; background:var(--red-bright); cursor:pointer; border:none; box-shadow:0 0 8px rgba(255,70,85,.5); }
@media (prefers-reduced-motion: reduce){ .rv-eye-instance,.fx-cross{ display:none; } }
</style>
</head>
<body class="dark">
<!-- Фоновые слои -->
<div class="rv-bg" aria-hidden="true">
<div class="rv-vignette"></div>
<div class="rv-pattern" id="rv-pattern"></div>
<div class="rv-eye-wrap">
<div class="rv-eye" id="rv-eye">
<svg viewBox="0 0 400 200" fill="none" stroke="currentColor" stroke-width="3">
<path d="M20 100 Q200 -20 380 100 Q200 220 20 100Z"/>
<circle cx="200" cy="100" r="52"/>
<path d="M200 62 l10 28 28 10 -28 10 -10 28 -10 -28 -28 -10 28 -10z" fill="currentColor" stroke="none"/>
<path d="M200 30 v-14 M200 170 v14 M120 55 l-12-10 M280 55 l12-10 M120 145 l-12 10 M280 145 l12 10" stroke-linecap="round"/>
</svg>
</div>
</div>
<div class="rv-fx" id="rv-fx"></div>
</div>
<canvas id="dust-canvas" aria-hidden="true"></canvas>
<!-- Музыкальный контрол -->
<div class="music-control">
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="50">
<button class="music-btn" id="music-toggle" title="Музыка">
<svg id="music-icon-on" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
<svg id="music-icon-off" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="display:none;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" />
</svg>
</button>
</div>
<audio id="bg-music" src="hoyo_.mp3" loop preload="auto"></audio>
<div id="app-container" class="relative z-10 container mx-auto px-4 py-8 md:py-12 max-w-5xl">
<header class="text-center mb-12">
<div class="flex justify-between items-center mb-6">
<button id="theme-toggle" class="rv-iconbtn p-3 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</button>
<button id="settings-toggle" class="rv-iconbtn p-3 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
<h1 class="rv-title text-5xl md:text-6xl font-bold mb-4">Рулетка Genshin Impact</h1>
<div class="ornament"><span>✕</span></div>
<p class="rv-sub text-lg max-w-2xl mx-auto">Случайный выбор персонажей, боссов и местных легенд</p>
</header>
<main>
<div id="settings-panel" class="settings-panel cornered p-8 mb-8">
<h3 class="text-2xl font-bold mb-6 rv-label">Настройки рулетки</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div>
<label for="team-size" class="block text-sm font-bold mb-3 rv-label">Количество персонажей в отряде</label>
<select id="team-size" class="rv-input w-full">
<option value="4">4</option><option value="3">3</option><option value="2">2</option><option value="1">1</option>
</select>
</div>
<div>
<label for="animation-speed" class="block text-sm font-bold mb-3 rv-label">Скорость анимации</label>
<select id="animation-speed" class="rv-input w-full">
<option value="fast">Быстрая</option><option value="medium" selected>Средняя</option><option value="slow">Медленная</option>
</select>
</div>
</div>
<h3 class="text-2xl font-bold mb-4 rv-label">Мои персонажи</h3>
<p class="rv-sub text-sm mb-6">Выберите персонажей, которыми вы владеете, чтобы рулетка использовала только их. Если ни один не выбран, будут использоваться все персонажи.</p>
<div class="mb-6"><input type="text" id="character-search" placeholder="Поиск персонажей..." class="rv-input w-full"></div>
<div id="player-characters-selection" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-4 mb-8 max-h-80 overflow-y-auto pr-3 custom-scrollbar"></div>
<div class="text-center">
<button id="clear-player-characters" class="rv-btn rv-btn-danger py-3 px-6 rounded-lg text-sm hidden">Очистить выбранных персонажей</button>
</div>
<h3 class="text-2xl font-bold mb-4 mt-8 rv-label">Настройки боссов</h3>
<p class="rv-sub text-sm mb-6">Выберите, какие боссы и местные легенды должны участвовать в рандомизации</p>
<div class="mb-6">
<div class="legend-settings">
<label for="boss-category-select" class="rv-label">Категория:</label>
<select id="boss-category-select" class="rv-input w-full">
<option value="world-bosses">Мировые боссы</option>
<option value="weekly-bosses">Еженедельные боссы</option>
<option value="local-legends">Местные легенды</option>
</select>
</div>
</div>
<div id="bosses-settings-container" class="max-h-96 overflow-y-auto pr-3 custom-scrollbar"></div>
</div>
<div class="category-select cornered">
<label for="category-select" class="rv-label">Выберите категорию для рандомизации:</label>
<select id="category-select" class="rv-input w-full">
<option value="characters">Отряд персонажей</option>
<option value="world-bosses">Мировые боссы</option>
<option value="weekly-bosses">Еженедельные боссы</option>
<option value="local-legends">Местные легенды</option>
</select>
</div>
<section id="characters-section" class="mb-20 section-panel cornered p-8">
<h2 class="text-4xl font-bold text-center mb-4 rv-label">Сформировать отряд персонажей</h2>
<div class="ornament"><span>✕</span></div>
<div class="flex justify-center mb-10">
<button id="spin-characters-button" class="rv-btn rv-btn-primary py-5 px-12 rounded-lg text-xl">Крутить отряд!</button>
</div>
<div id="characters-result" class="min-h-[200px] result-panel p-6 flex flex-wrap justify-center items-center gap-6 transition-all duration-300 flex-col sm:flex-row">
<p id="characters-placeholder" class="rv-sub">Ваш случайный отряд появится здесь.</p>
<div id="characters-loader" class="loader hidden"></div>
</div>
<div id="save-characters-section" class="mt-6 flex flex-col sm:flex-row justify-center items-center gap-4 hidden">
<input type="text" id="team-name-input" placeholder="Название отряда (необязательно)" class="rv-input w-full sm:w-auto flex-grow">
<button id="save-team-button" class="rv-btn py-3 px-8 rounded-lg w-full sm:w-auto">Сохранить отряд</button>
</div>
</section>
<section id="world-bosses-section" class="mb-20 section-panel cornered p-8 hidden">
<h2 class="text-4xl font-bold text-center mb-4 rv-label">Рандом мирового босса</h2>
<div class="ornament"><span>✕</span></div>
<div class="flex justify-center mb-10">
<button id="spin-world-boss-button" class="rv-btn rv-btn-primary py-5 px-12 rounded-lg text-xl">Выбрать босса!</button>
</div>
<div id="world-boss-result" class="min-h-[200px] result-panel p-6 flex flex-wrap justify-center items-center gap-6 transition-all duration-300 flex-col sm:flex-row">
<p id="world-boss-placeholder" class="rv-sub">Случайный мировой босс появится здесь.</p>
<div id="world-boss-loader" class="loader hidden"></div>
</div>
<div id="save-world-boss-section" class="mt-6 flex justify-center hidden">
<button id="save-world-boss-button" class="rv-btn py-3 px-8 rounded-lg">Сохранить в историю</button>
</div>
</section>
<section id="weekly-bosses-section" class="mb-20 section-panel cornered p-8 hidden">
<h2 class="text-4xl font-bold text-center mb-4 rv-label">Рандом еженедельного босса</h2>
<div class="ornament"><span>✕</span></div>
<div class="flex justify-center mb-10">
<button id="spin-weekly-boss-button" class="rv-btn rv-btn-primary py-5 px-12 rounded-lg text-xl">Выбрать босса!</button>
</div>
<div id="weekly-boss-result" class="min-h-[200px] result-panel p-6 flex flex-wrap justify-center items-center gap-6 transition-all duration-300 flex-col sm:flex-row">
<p id="weekly-boss-placeholder" class="rv-sub">Случайный еженедельный босс появится здесь.</p>
<div id="weekly-boss-loader" class="loader hidden"></div>
</div>
<div id="save-weekly-boss-section" class="mt-6 flex justify-center hidden">
<button id="save-weekly-boss-button" class="rv-btn py-3 px-8 rounded-lg">Сохранить в историю</button>
</div>
</section>
<section id="local-legends-section" class="mb-20 section-panel cornered p-8 hidden">
<h2 class="text-4xl font-bold text-center mb-4 rv-label">Рандом местной легенды</h2>
<div class="ornament"><span>✕</span></div>
<div class="flex justify-center mb-10">
<button id="spin-legend-button" class="rv-btn rv-btn-primary py-5 px-12 rounded-lg text-xl">Выбрать легенду!</button>
</div>
<div id="legend-result" class="min-h-[200px] result-panel p-6 flex flex-wrap justify-center items-center gap-6 transition-all duration-300 flex-col sm:flex-row">
<p id="legend-placeholder" class="rv-sub">Случайная местная легенда появится здесь.</p>
<div id="legend-loader" class="loader hidden"></div>
</div>
<div id="save-legend-section" class="mt-6 flex justify-center hidden">
<button id="save-legend-button" class="rv-btn py-3 px-8 rounded-lg">Сохранить в историю</button>
</div>
</section>
<section id="history-section" class="section-panel cornered p-8">
<div class="flex justify-between items-center mb-10">
<h2 class="text-4xl font-bold rv-label">История</h2>
<div class="flex gap-3">
<button id="show-full-history-button" class="rv-btn py-3 px-6 rounded-lg text-sm hidden">Вся история</button>
<button id="clear-history-button" class="rv-btn rv-btn-danger py-3 px-6 rounded-lg text-sm hidden">Очистить</button>
</div>
</div>
<div id="history-content" class="flex flex-col gap-8">
<p id="history-placeholder" class="text-center rv-sub">История ваших случайных выборов будет отображаться здесь.</p>
</div>
</section>
</main>
<footer class="text-center mt-20 pt-10" style="border-top:1px solid var(--line-soft);">
<div class="ornament"><span>✕</span></div>
<p class="rv-sub text-sm">Это фанатский проект. Не аффилирован и не одобрен HoYoverse.</p>
</footer>
</div>
<div id="confirm-modal" class="fixed inset-0 bg-black bg-opacity-80 z-50 flex justify-center items-center hidden p-4">
<div class="modal-panel cornered w-full max-w-sm shadow-2xl relative text-center p-8 transform transition-all duration-300 scale-95 opacity-0" id="confirm-modal-content">
<h3 class="text-3xl font-bold mb-6 rv-label">Подтверждение</h3>
<p id="confirm-modal-message" class="text-lg mb-8 rv-sub">Вы уверены, что хотите очистить историю?</p>
<div class="flex justify-around gap-4">
<button id="confirm-modal-cancel" class="rv-btn py-3 px-8 rounded-lg">Отмена</button>
<button id="confirm-modal-ok" class="rv-btn rv-btn-danger py-3 px-8 rounded-lg">Очистить</button>
</div>
</div>
</div>
<div id="full-history-modal" class="fixed inset-0 bg-black bg-opacity-80 z-50 flex justify-center items-center hidden p-4">
<div class="modal-panel cornered w-full max-w-5xl h-[90vh] shadow-2xl relative flex flex-col p-8 transform transition-all duration-300 scale-95 opacity-0" id="full-history-modal-content">
<h2 class="text-4xl md:text-5xl font-bold text-center mb-4 rv-label">Вся История</h2>
<div class="ornament"><span>✕</span></div>
<div id="full-history-modal-content-inner" class="flex flex-col gap-8 overflow-y-auto flex-grow pr-4 custom-scrollbar"></div>
<div class="text-center mt-8">
<button id="full-history-modal-close" class="rv-btn py-3 px-8 rounded-lg">Закрыть</button>
</div>
</div>
</div>
<audio id="boom-sound" src="boom.mp3" preload="auto" volume="1.0"></audio>
<script>
document.addEventListener('DOMContentLoaded', () => {
const LOCAL_CHARACTERS = [
{ "name": "Альбедо", "icon": "pic/albedo.png", "element": "Geo" },
{ "name": "Аль-Хайтам", "icon": "pic/alhaitham.png", "element": "Dendro" },
{ "name": "Алой", "icon": "pic/aloy.png", "element": "Cryo" },
{ "name": "Эмбер", "icon": "pic/amber.png", "element": "Pyro" },
{ "name": "Аратаки Итто", "icon": "pic/arataki_itto.png", "element": "Geo" },
{ "name": "Арлекино", "icon": "pic/arlecchino.png", "element": "Pyro" },
{ "name": "Бай Чжу", "icon": "pic/baizhu.png", "element": "Dendro" },
{ "name": "Барбара", "icon": "pic/barbara.png", "element": "Hydro" },
{ "name": "Бэй Доу", "icon": "pic/beidou.png", "element": "Electro" },
{ "name": "Беннет", "icon": "pic/bennett.png", "element": "Pyro" },
{ "name": "Кандакия", "icon": "pic/candace.png", "element": "Hydro" },
{ "name": "Шарлотта", "icon": "pic/charlotte.png", "element": "Cryo" },
{ "name": "Часка", "icon": "pic/chasca.png", "element": "Anemo" },
{ "name": "Тиори", "icon": "pic/chiori.png", "element": "Geo" },
{ "name": "Чун Юнь", "icon": "pic/chongyun.png", "element": "Cryo" },
{ "name": "Ситлали", "icon": "pic/citlali.png", "element": "Cryo" },
{ "name": "Клоринда", "icon": "pic/clorinde.png", "element": "Electro" },
{ "name": "Коллеи", "icon": "pic/collei.png", "element": "Dendro" },
{ "name": "Сайно", "icon": "pic/cyno.png", "element": "Electro" },
{ "name": "Дэхья", "icon": "pic/dehya.png", "element": "Pyro" },
{ "name": "Дилюк", "icon": "pic/diluc.png", "element": "Pyro" },
{ "name": "Диона", "icon": "pic/diona.png", "element": "Cryo" },
{ "name": "Дори", "icon": "pic/dori.png", "element": "Electro" },
{ "name": "Эмилия", "icon": "pic/emilie.png", "element": "Dendro" },
{ "name": "Эскофье", "icon": "pic/escoffier.png", "element": "Cryo" },
{ "name": "Эола", "icon": "pic/eula.png", "element": "Cryo" },
{ "name": "Фарузан", "icon": "pic/faruzan.png", "element": "Anemo" },
{ "name": "Фишль", "icon": "pic/fischl.png", "element": "Electro" },
{ "name": "Фремине", "icon": "pic/freminet.png", "element": "Cryo" },
{ "name": "Фурина", "icon": "pic/furina.png", "element": "Hydro" },
{ "name": "Га Мин", "icon": "pic/gaming.png", "element": "Pyro" },
{ "name": "Гань Юй", "icon": "pic/ganyu.png", "element": "Cryo" },
{ "name": "Горо", "icon": "pic/gorou.png", "element": "Geo" },
{ "name": "Ху Тао", "icon": "pic/hu_tao.png", "element": "Pyro" },
{ "name": "Ифу", "icon": "pic/ifa.png", "element": "Anemo" },
{ "name": "Иансан", "icon": "pic/iansan.png", "element": "Electro" },
{ "name": "Джинн", "icon": "pic/jean.png", "element": "Anemo" },
{ "name": "Кадзуха", "icon": "pic/kaedehara_kazuha.png", "element": "Anemo" },
{ "name": "Кэйа", "icon": "pic/kaeya.png", "element": "Cryo" },
{ "name": "Качина", "icon": "pic/Kachina.webp", "element": "Geo" },
{ "name": "Аяка", "icon": "pic/kamisato_ayaka.png", "element": "Cryo" },
{ "name": "Аято", "icon": "pic/kamisato_ayato.png", "element": "Hydro" },
{ "name": "Кавэх", "icon": "pic/kaveh.png", "element": "Dendro" },
{ "name": "Кэ Цин", "icon": "pic/keqing.png", "element": "Electro" },
{ "name": "Кинич", "icon": "pic/kinich.png", "element": "Dendro" },
{ "name": "Кирара", "icon": "pic/kirara.png", "element": "Dendro" },
{ "name": "Кли", "icon": "pic/klee.png", "element": "Pyro" },
{ "name": "Сара", "icon": "pic/kujou_sara.png", "element": "Electro" },
{ "name": "Шинобу", "icon": "pic/kuki_shinobu.png", "element": "Electro" },
{ "name": "Лань Янь", "icon": "pic/lan_yan.png", "element": "Anemo" },
{ "name": "Лейла", "icon": "pic/layla.png", "element": "Cryo" },
{ "name": "Лиза", "icon": "pic/lisa.png", "element": "Electro" },
{ "name": "Линнет", "icon": "pic/lynette.png", "element": "Anemo" },
{ "name": "Лини", "icon": "pic/lyney.png", "element": "Pyro" },
{ "name": "Мавуика", "icon": "pic/mavuika.png", "element": "Pyro" },
{ "name": "Мика", "icon": "pic/mika.png", "element": "Cryo" },
{ "name": "Мона", "icon": "pic/mona.png", "element": "Hydro" },
{ "name": "Муалани", "icon": "pic/mualani.png", "element": "Hydro" },
{ "name": "Нахида", "icon": "pic/nahida.png", "element": "Dendro" },
{ "name": "Навия", "icon": "pic/navia.png", "element": "Geo" },
{ "name": "Нёвиллет", "icon": "pic/neuvillette.png", "element": "Hydro" },
{ "name": "Нилу", "icon": "pic/nilou.png", "element": "Hydro" },
{ "name": "Нин Гуан", "icon": "pic/ningguang.png", "element": "Geo" },
{ "name": "Ноэлль", "icon": "pic/noelle.png", "element": "Geo" },
{ "name": "Оророн", "icon": "pic/ororon.png", "element": "Electro" },
{ "name": "Ци Ци", "icon": "pic/qiqi.png", "element": "Cryo" },
{ "name": "Райдэн", "icon": "pic/raiden_shogun.png", "element": "Electro" },
{ "name": "Рэйзор", "icon": "pic/razor.png", "element": "Electro" },
{ "name": "Розалья", "icon": "pic/rosaria.png", "element": "Cryo" },
{ "name": "Кокоми", "icon": "pic/sangonomiya_kokomi.png", "element": "Hydro" },
{ "name": "Саю", "icon": "pic/sayu.png", "element": "Anemo" },
{ "name": "Сетос", "icon": "pic/sethos.png", "element": "Electro" },
{ "name": "Шэнь Хэ", "icon": "pic/shenhe.png", "element": "Cryo" },
{ "name": "Хэйдзо", "icon": "pic/shikanoin_heizo.png", "element": "Anemo" },
{ "name": "Сиджвин", "icon": "pic/sigewinne.png", "element": "Hydro" },
{ "name": "Сахароза", "icon": "pic/sucrose.png", "element": "Anemo" },
{ "name": "Тарталья", "icon": "pic/tartaglia.png", "element": "Hydro" },
{ "name": "Тома", "icon": "pic/thoma.png", "element": "Pyro" },
{ "name": "Тигнари", "icon": "pic/tighnari.png", "element": "Dendro" },
{ "name": "Вареса", "icon": "pic/varesa.png", "element": "Electro" },
{ "name": "Венти", "icon": "pic/venti.png", "element": "Anemo" },
{ "name": "Странник", "icon": "pic/wanderer.png", "element": "Anemo" },
{ "name": "Ризли", "icon": "pic/wriothesley.png", "element": "Cryo" },
{ "name": "Сяо", "icon": "pic/xiao.png", "element": "Anemo" },
{ "name": "Сян Линь", "icon": "pic/xiangling.png", "element": "Pyro" },
{ "name": "Сянь Юнь", "icon": "pic/xianyun.png", "element": "Anemo" },
{ "name": "Шилонен", "icon": "pic/xilonen.png", "element": "Geo" },
{ "name": "Син Цю", "icon": "pic/xingqiu.png", "element": "Hydro" },
{ "name": "Синь Янь", "icon": "pic/xinyan.png", "element": "Pyro" },
{ "name": "Яэ Мико", "icon": "pic/yae_miko.png", "element": "Electro" },
{ "name": "Янь Фэй", "icon": "pic/yanfei.png", "element": "Pyro" },
{ "name": "Яо Яо", "icon": "pic/yaoyao.png", "element": "Dendro" },
{ "name": "Е Лань", "icon": "pic/yelan.png", "element": "Hydro" },
{ "name": "Ёимия", "icon": "pic/yoimiya.png", "element": "Pyro" },
{ "name": "Юнь Цзинь", "icon": "pic/yun_jin.png", "element": "Geo" },
{ "name": "Паймон", "icon": "pic/paimon.png", "element": "Anemo" },
{ "name": "ГГ(Анемо)", "icon": "pic/traveler.webp", "element": "Anemo" },
{ "name": "ГГ (Гео)", "icon": "pic/traveler.webp", "element": "Geo" },
{ "name": "ГГ (Электро)", "icon": "pic/traveler.webp", "element": "Electro" },
{ "name": "ГГ (Дендро)", "icon": "pic/traveler.webp", "element": "Dendro" },
{ "name": "ГГ (Гидро)", "icon": "pic/traveler.webp", "element": "Hydro" },
{ "name": "Мидзуки", "icon": "pic/yumemizuki_mizuki.png", "element": "Anemo" },
{ "name": "Чжун Ли", "icon": "pic/zhongli.png", "element": "Geo" },
{ "name": "Скирк", "icon": "pic/skirk.png", "element": "Cryo" },
{ "name": "Далия", "icon": "pic/daliya.png", "element": "Hydro" },
{ "name": "Инеффа", "icon": "pic/ineffa.png", "element": "Electro" },
{ "name": "Лаума", "icon": "pic/lauma_icon.webp", "element": "Dendro" },
{ "name": "Айно", "icon": "pic/aino_icon.webp", "element": "Hydro" },
{ "name": "Флинс", "icon": "pic/flins_icon.webp", "element": "Electro" },
{ "name": "Нефер", "icon": "pic/Nefer.webp", "element": "Dendro" },
{ "name": "Ягода", "icon": "pic/YAgoda.webp", "element": "Anemo" },
{ "name": "Дурин", "icon": "pic/Durin.webp", "element": "Pyro" },
{ "name": "Иллуги", "icon": "pic/Illuga.png", "element": "Geo" },
{ "name": "Цзы Бай", "icon": "pic/zibai.png", "element": "Geo" },
{ "name": "Коломбина", "icon": "pic/Columbina.png", "element": "Hydro" },
{ "name": "Варка", "icon": "pic/varka.png", "element": "Anemo" },
{ "name": "Прюн", "icon": "pic/prune.png", "element": "Anemo" },
{ "name": "Линнея", "icon": "pic/linneya.png", "element": "Geo" },
{ "name": "Николь", "icon": "pic/nikol.png", "element": "Pyro" },
{ "name": "Лохэн", "icon": "pic/lohen.png", "element": "Cryo" },
{ "name": "Сандроне", "icon": "pic/sandrone.png", "element": "Cryo" },
{ "name": "Алёша", "icon": "pic/alyesha.png", "element": "Electro" },
{ "name": "ГГ (Крио)", "icon": "pic/traveler.webp", "element": "Cryo" },
{ "name": "Одетта", "icon": "pic/odetta.png", "element": "Cryo" },
{ "name": "Весна", "icon": "pic/vesna.png", "element": "Anemo" },
{ "name": "Водяница", "icon": "pic/vodyanitsa.png", "element": "Hydro" },
];
const WORLD_BOSSES = [
{ "name": "Сверхтяжёлый сухопутный крейсер: Мобильная крепость", "icon": "pic/WorldBoss/Sverkhtyazhyelyy-sukhoputnyy-kreyser.webp" },
{ "name": "Повелитель морозной ночи", "icon": "pic/WorldBoss/Povelitel-moroznoy-nochi.webp" },
{ "name": "Лучезарный мотылёк-призрак", "icon": "pic/WorldBoss/Luchezarnyy-motylyek_prizrak.webp" },
{ "name": "Железный утёнок", "icon": "pic/WorldBoss/ZHeleznyy-utyenok.webp" },
{ "name": "Автоматон: Надсмотрщик", "icon": "pic/WorldBoss/Secret-Source-Automaton-Overseer-Device.webp" },
{ "name": "Статуя лавового дракона", "icon": "pic/WorldBoss/Statuya-lavovogo-drakona.webp" },
{ "name": "Говорящая с духами отшельница", "icon": "pic/WorldBoss/Govoryashchaya-s-dukhami-otshelnitsa.webp" },
{ "name": "Сумрачная папилла", "icon": "pic/WorldBoss/Sumrachnaya-papilla.webp" },
{ "name": "Автоматон: Конфигуратор", "icon": "pic/WorldBoss/Avtomaton-tainstvennogo-istochnika.webp" },
{ "name": "Прожорливый Горный король-юмказавр", "icon": "pic/WorldBoss/Prozhorlivyy-gornyy-korol-yumkazavr.webp" },
{ "name": "Кукузавр-тиран золотого пламени", "icon": "pic/WorldBoss/Prozhorlivyi_-Gornyi_-korol_yumkazavr.webp" },
{ "name": "Легат големов", "icon": "pic/WorldBoss/Kapitan-golemov.webp" },
{ "name": "Суаньни-отшельник", "icon": "pic/WorldBoss/Suanni_otshelnik.webp" },
{ "name": "Гидротульпа", "icon": "pic/WorldBoss/Probuzhdyennaya-zhizn.webp" },
{ "name": "Экспериментальный генератор поля", "icon": "pic/WorldBoss/Eksperimentalnyy-generator-polya.webp" },
{ "name": "Морской конёк тысячелетней жемчужины", "icon": "pic/WorldBoss/Morskoy-konyek-tysyacheletney-zhemchuzhiny.webp" },
{ "name": "Император огня и железа", "icon": "pic/WorldBoss/Imperator-ognya-i-zheleza.webp" },
{ "name": "Сюита ледяного ветра", "icon": "pic/WorldBoss/Syuita-ledyanogo-vetra.webp" },
{ "name": "Чудовищный креститель", "icon": "pic/WorldBoss/CHudovishchnyy-krestitel.webp" },
{ "name": "Унут Сетеха", "icon": "pic/WorldBoss/Unut-Setekha.webp" },
{ "name": "Дендро Гипостазис", "icon": "pic/WorldBoss/Dendro-Gipostazis.png" },
{ "name": "Алгоритм полуизвечной матрицы оператора наблюдения", "icon": "pic/WorldBoss/Poluvechnaya-matritsa-upravleniya.webp" },
{ "name": "Дракон вечных бедствий", "icon": "pic/WorldBoss/Drakon-vechnykh-bedstviy.webp" },
{ "name": "Электро папоротник", "icon": "pic/WorldBoss/Elektro-paporotnik.webp" },
{ "name": "Пернатый плесенник", "icon": "pic/WorldBoss/Pernatyy-plesennik.webp" },
{ "name": "Змей руин", "icon": "pic/WorldBoss/Zmey-ruin.webp" },
{ "name": "Стая вишапов глубин", "icon": "pic/WorldBoss/Staya-vishapov-glubin.webp" },
{ "name": "Золотой волчий вожак", "icon": "pic/WorldBoss/Golden-Wolflord.png" },
{ "name": "Гидро Гипостазис", "icon": "pic/WorldBoss/Gidro-Gipostazis.png" },
{ "name": "Манифестация грома", "icon": "pic/WorldBoss/Elektro-Okeanid.png" },
{ "name": "Бесконечный механический массив", "icon": "pic/WorldBoss/Beskonechnyy-mekhanicheskiy-massiv.png" },
{ "name": "Пиро гипостазис", "icon": "pic/WorldBoss/Piro-gipostazis.png" },
{ "name": "Магу Кенки", "icon": "pic/WorldBoss/Magu-Kenki.webp" },
{ "name": "Крио гипостазис", "icon": "pic/WorldBoss/Cryo_Hypostasis.webp" },
{ "name": "Древний геовишап", "icon": "pic/WorldBoss/Drevniy-geovishap.webp" },
{ "name": "Гео гипостазис", "icon": "pic/WorldBoss/UI_MonsterIcon_Effigy_Rock.png" },
{ "name": "Электро гипостазис", "icon": "pic/WorldBoss/UI_MonsterIcon_Effigy_Electric.png" },
{ "name": "Анемо гипостазис", "icon": "pic/WorldBoss/UI_MonsterIcon_Effigy_Wind.png" },
{ "name": "Крио папоротник", "icon": "pic/WorldBoss/UI_MonsterIcon_cryo_regisvine.png" },
{ "name": "Пиро орхидея", "icon": "pic/WorldBoss/UI_MonsterIcon_Regisvine_Fire.png" },
{ "name": "Океанид", "icon": "pic/WorldBoss/Okeanid.webp" },
{ "name": "Лучезарный лунный геккон", "icon": "pic/WorldBoss/Luchezarnyi_-lunnyi_-gekkon.webp" },
{ "name": "Повелитель скрытых глубин: Шёпот кошмаров", "icon": "pic/WorldBoss/Povelitel-skrytykh-glubin-SHyepot-koshmarov.webp" },
{ "name": "Созерцатель: Падший страж", "icon": "pic/WorldBoss/Sozertsatel-Padshiy-strazh.webp" },
{ "name": "Бессмертный конструкт", "icon": "pic/WorldBoss/Bessmertnyy-konstrukt.webp" },
{ "name": "Крылатая химера-лев", "icon": "pic/WorldBoss/Krylataya-khimera_lev.webp" }
];
const WEEKLY_BOSSES = [
{ "name": "Игра перед вратами", "icon": "pic/Allweeklybosses/Igra-pered-vratami.webp" },
{ "name": "Доктор", "icon": "pic/Allweeklybosses/Doktor.webp" },
{ "name": "Владыка истлевшего первобытного пламени", "icon": "pic/Allweeklybosses/Lord-of-Eroded-Primal-Fire.webp" },
{ "name": "Слуга", "icon": "pic/Allweeklybosses/Nomer-chetyre-iz-Predvestnikov-Fatui_-Arlekino.webp" },
{ "name": "Всепожирающий нарвал", "icon": "pic/Allweeklybosses/Vsepozhirayushchiy-narval.webp" },
{ "name": "Страж оазиса Апепа", "icon": "pic/Allweeklybosses/Strazh-oazisa-Apepa.webp" },
{ "name": "Сёки но Ками", "icon": "pic/Allweeklybosses/Syeki-no-Kami.webp" },
{ "name": "Магацу митакэ Наруками но микото", "icon": "pic/Allweeklybosses/m_29060201.webp" },
{ "name": "Синьора", "icon": "pic/Allweeklybosses/Sinora.png" },
{ "name": "Аждаха", "icon": "pic/Allweeklybosses/Azhdaha.png" },
{ "name": "Чайльд", "icon": "pic/Allweeklybosses/Childe.png" },
{ "name": "Лупус Бореалис, Волчий лорд", "icon": "pic/Allweeklybosses/UI_MonsterIcon_LupiBoreas.png" },
{ "name": "Ужас бури", "icon": "pic/Allweeklybosses/Stormterror.png" }
];
const LOCAL_LEGENDS = [
{ "name": "Волшебные рыцари-близнецы", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-284766-1-800x476.jpg", "guideUrl": "https://wotpack.ru/volshebnye-rycari-bliznecy-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Окружной судья океана", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-284964-4.jpg", "guideUrl": "https://wotpack.ru/okruzhnoj-sudja-okeana-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Дева озера Вивианна", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-283768-1-800x425.jpg", "guideUrl": "https://wotpack.ru/deva-ozera-vivianna-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Дева озера Нинианна", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-283708-1-800x423.jpg", "guideUrl": "https://wotpack.ru/deva-ozera-ninianna-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Угасающий старый солдат", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-283590-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/ugasajushhij-staryj-soldat-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Владыка отшельников Доварху", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-283520-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/vladyka-otshelnikov-dovarhu-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Меч ущелья", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-283554-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/mech-ushhelja-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Железный виконт", "icon": "https://wotpack.ru/wp-content/uploads/2023/08/word-image-284964-10.jpg", "guideUrl": "https://wotpack.ru/zheleznyj-vikont-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Автоматический генератор полей сверхвычислений", "icon": "https://wotpack.ru/wp-content/uploads/2023/09/word-image-301036-4.jpeg", "guideUrl": "https://wotpack.ru/avtomaticheskij-generator-polej-sverhvychislenij-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Лиахра Блистательная", "icon": "https://wotpack.ru/wp-content/uploads/2023/09/word-image-301327-4.jpeg", "guideUrl": "https://wotpack.ru/liahra-blistatelnaja-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Изольт", "icon": "https://wotpack.ru/wp-content/uploads/2023/09/word-image-301389-1-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/izolt-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Шассанион", "icon": "https://wotpack.ru/wp-content/uploads/2023/11/word-image-321383-7-800x450.jpeg", "guideUrl": "https://wotpack.ru/shassanion-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Деянейра Снежневна", "icon": "https://wotpack.ru/wp-content/uploads/2023/11/1-10-800x450.jpg", "guideUrl": "https://wotpack.ru/dejanejra-snezhnevna-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Роки Авилдсен", "icon": "https://wotpack.ru/wp-content/uploads/2023/11/word-image-321618-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/roki-avildsen-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Магический меч Круж", "icon": "https://wotpack.ru/wp-content/uploads/2023/11/word-image-322447-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/magicheskij-mech-kruzh-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Лиям", "icon": "https://wotpack.ru/wp-content/uploads/2023/11/word-image-321469-1-800x450.jpeg", "guideUrl": "https://wotpack.ru/lijam-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Киней", "icon": "https://wotpack.ru/wp-content/uploads/2024/04/Kinej-v-Genshin-Impact-800x450.jpg", "guideUrl": "https://wotpack.ru/kinej-v-genshin-impact-kak-najti-i-pobedit/" },
{ "name": "Три полихромные звезды", "icon": "https://wotpack.ru/wp-content/uploads/2024/08/mestnaya-legenda-tri-polihromnye-zvezdy-3-800x449.jpg", "guideUrl": "https://wotpack.ru/tri-polihromnye-zvezdy-v-genshin-impact-kak-nayti-i-pobedit-vasiliya-sidorenko-i-nomonkonova/" },
{ "name": "Косихо", "icon": "https://wotpack.ru/wp-content/uploads/2024/08/kosiho-v-genshin-impact-kak-nayti-i-pobedit-800x450.jpg", "guideUrl": "https://wotpack.ru/kosiho-v-genshin-impact-kak-nayti-i-pobedit-cherepahu/" },
{ "name": "Сапфо среди волн", "icon": "https://wotpack.ru/wp-content/uploads/2024/08/sapfo-sredi-voln-v-genshin-impact-kak-nayti-i-pobedit-800x450.jpg", "guideUrl": "https://wotpack.ru/sapfo-sredi-voln-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Балачко", "icon": "https://wotpack.ru/wp-content/uploads/2024/08/balachko-v-genshin-impact-kak-nayti-i-pobedit-397x206.jpg", "guideUrl": "https://wotpack.ru/balachko-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Рилай", "icon": "https://wotpack.ru/wp-content/uploads/2024/11/rilay-v-genshin-impact-800x450.jpg", "guideUrl": "https://wotpack.ru/rilay-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Короля кабанов", "icon": "https://wotpack.ru/wp-content/uploads/2021/11/%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D1%8C-%D1%81%D0%BD%D0%B5%D0%B6%D0%BD%D1%8B%D1%85-%D0%BA%D0%B0%D0%B1%D0%B0%D0%BD%D0%BE%D0%B2-348x220.jpg", "guideUrl": "https://wotpack.ru/genshin-impact-ah-svezhee-mjaso-prohozhdenie-kvesta-gajd/" },
{ "name": "Бронзовый замок", "icon": "https://wotpack.ru/wp-content/uploads/2024/11/bronzovyy-2.jpg", "guideUrl": "https://wotpack.ru/bronzovyy-zamok-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Ничтожный", "icon": "https://wotpack.ru/wp-content/uploads/2025/03/nichtozhnyy-v-mire-vid.jpg", "guideUrl": "https://wotpack.ru/nichtozhnyy-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Пик", "icon": "https://wotpack.ru/wp-content/uploads/2025/03/pik-v-genshin-impact-kak-pobedit-mutanta-730x411.jpg", "guideUrl": "https://wotpack.ru/pik-v-genshin-impact-kak-nayti-i-pobedit-mutanta/" },
{ "name": "Боевой козел и Железный коготь", "icon": "https://wotpack.ru/wp-content/uploads/2025/03/boevoy-kozyol-i-zheleznyy-kogot-v-genshin-impact_-kak-nayti-i-pobedit-730x411.jpg", "guideUrl": "https://wotpack.ru/boevoy-kozyol-i-zheleznyy-kogot-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Теночцитока", "icon": "https://wotpack.ru/wp-content/uploads/2025/07/posledniy-vyzhivshiy-iz-tenochtsitoka-v-genshin-impact-730x455.jpg", "guideUrl": "https://wotpack.ru/posledniy-vyzhivshiy-iz-tenochtsitoka-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Царь-краб, хозяин дворца", "icon": "https://wotpack.ru/wp-content/uploads/2025/09/word-image-546429-2.jpeg", "guideUrl": "https://wotpack.ru/tsarskiy-krab-v-genshin-impact-kak-pobedit-i-proyti-ispytanie/" },
{ "name": "Сигурд", "icon": "https://wotpack.ru/wp-content/uploads/2025/09/sigurd-v-genshin-impact-730x411.jpg", "guideUrl": "https://wotpack.ru/sigurd-v-genshin-impact-kak-pobedit-i-proyti-ispytanie/" },
{ "name": "Раскольников", "icon": "https://wotpack.ru/wp-content/uploads/2025/09/raskolnikov-v-genshin-impact-730x411.jpg", "guideUrl": "https://wotpack.ru/raskolnikov-v-genshin-impact-kak-pobedit-i-proyti-ispytanie/" },
{ "name": "Ицзи и Тяньюнь", "icon": "https://wotpack.ru/wp-content/uploads/2024/01/20240131070630.jpg", "guideUrl": "https://wotpack.ru/vladyki-gor-chichzhan-v-genshin-impact-kak-najti-i-pobedit-iczi-i-tjanjun/" },
{ "name": "Шестнадцатикратная мандрагора", "icon": "pic/LocalLegends/placeholder.webp", "guideUrl": "https://wotpack.ru/shestnadtsatikratnaya-mandragora-v-genshin-impact-kak-pobedit-i-proyti-ispytanie/" },
{ "name": "Старая ведьма Хильетта", "icon": "https://wotpack.ru/wp-content/uploads/2026/01/hiletta-v-genshin-impact-730x390.jpg", "guideUrl": "https://wotpack.ru/hiletta-v-genshin-impact-kak-pobedit-babushku-i-proyti-ispytanie/" },
{ "name": "Тоскующий по родине волк-одиночка", "icon": "https://wotpack.ru/wp-content/uploads/2026/01/kartochka-toskuyuschego-po-rodine-volka-odinochki-u-kateriny-730x411.jpg", "guideUrl": "https://wotpack.ru/toskuyuschiy-po-rodine-volk-odinochka-v-genshin-impact-kak-pobedit-klyk-pustoshey/" },
{ "name": "Автоматон Муштешир", "icon": "https://wotpack.ru/wp-content/uploads/2026/07/mushteshir-v-genshin-impact-kak-pobedit-i-unichtozhit-chetyre-mehanizma-800x450.jpg", "guideUrl": "https://wotpack.ru/mushteshir-v-genshin-impact-kak-pobedit-i-unichtozhit-chetyre-mehanizma/" },
{ "name": "Призматический слайм «Отблеск творения»", "icon": "pic/LocalLegends/placeholder.webp", "guideUrl": "https://wotpack.ru/byvalyy-prizmaticheskiy-slaym-v-genshin-impact-kak-nayti-i-pobedit/" },
{ "name": "Чурльдрик", "icon": "https://wotpack.ru/wp-content/uploads/2026/04/churldrik.-kartochka-v-hronikah-doblesti-800x450.jpg", "guideUrl": "https://wotpack.ru/churldrik-v-genshin-impact-kak-pobedit-i-unichtozhit-vkusnyy-kotelok/" },
{ "name": "Маха Васудеваяпутра", "icon": "https://wotpack.ru/wp-content/uploads/2026/04/maha-vasudevayaputra-v-genshin-impact-kak-pobedit-do-fazy-obozhzheniya-800x445.jpg", "guideUrl": "https://wotpack.ru/maha-vasudevayaputra-v-genshin-impact-kak-pobedit-do-fazy-obozhzheniya/" },
{ "name": "Фуриоса", "icon": "https://wotpack.ru/wp-content/uploads/2026/08/furiosa-v-menyu-kateriny-nagrady-800x450.jpg", "guideUrl": "https://wotpack.ru/furiosa-v-genshin-impact-kak-pobedit-i-unichtozhit-korni-pregrady/" },
{ "name": "Чурин", "icon": "https://wotpack.ru/wp-content/uploads/2026/08/churin-v-genshin-impact-800x450.jpg", "guideUrl": "https://wotpack.ru/churin-v-genshin-impact-gde-nayti-i-kak-pobedit-uchenika-vayatelya/" },
{ "name": "Брандашмыг", "icon": "https://wotpack.ru/wp-content/uploads/2026/08/himera-razboynik-v-genshin-impact-800x443.jpg", "guideUrl": "https://wotpack.ru/himera-razboynik-v-genshin-impact-kak-pobedit-v-gigantskoy-forme-i-umenshennom-sostoyanii/" }
];
function getOrCreateUID() {
let uid = localStorage.getItem('genshinRouletteUID');
if (!uid) {
uid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
localStorage.setItem('genshinRouletteUID', uid);
}
return uid;
}
const currentUserUID = getOrCreateUID();
const config = {
teamSize: parseInt(localStorage.getItem(`${currentUserUID}_teamSize`)) || 4,
animationSpeed: localStorage.getItem(`${currentUserUID}_animationSpeed`) || 'medium',
darkMode: localStorage.getItem('theme') !== 'light',
category: localStorage.getItem('genshinCategory') || 'characters',
excludedBosses: JSON.parse(localStorage.getItem(`${currentUserUID}_excludedBosses`)) || { 'world-bosses': [], 'weekly-bosses': [], 'local-legends': [] }
};
let allCharacters = [...LOCAL_CHARACTERS];
let historyTeams = JSON.parse(localStorage.getItem(`${currentUserUID}_genshinTeamsHistory`)) || [];
let historyBosses = JSON.parse(localStorage.getItem(`${currentUserUID}_genshinBossesHistory`)) || [];
let playerCharacters = JSON.parse(localStorage.getItem(`${currentUserUID}_playerCharacters`)) || [];
let isSpinning = false, currentGeneratedTeam = [], currentGeneratedBoss = null, isWorldBoss = true, isLegend = false;
const $ = id => document.getElementById(id);
const spinCharactersButton=$('spin-characters-button'), spinWorldBossButton=$('spin-world-boss-button'), spinWeeklyBossButton=$('spin-weekly-boss-button'), spinLegendButton=$('spin-legend-button'),
categorySelect=$('category-select'), charactersResult=$('characters-result'), worldBossResult=$('world-boss-result'), weeklyBossResult=$('weekly-boss-result'), legendResult=$('legend-result'),
charactersPlaceholder=$('characters-placeholder'), worldBossPlaceholder=$('world-boss-placeholder'), weeklyBossPlaceholder=$('weekly-boss-placeholder'), legendPlaceholder=$('legend-placeholder'),
charactersLoader=$('characters-loader'), worldBossLoader=$('world-boss-loader'), weeklyBossLoader=$('weekly-boss-loader'), legendLoader=$('legend-loader'),
historyContent=$('history-content'), historyPlaceholder=$('history-placeholder'), clearHistoryButton=$('clear-history-button'), showFullHistoryButton=$('show-full-history-button'),
themeToggle=$('theme-toggle'), settingsToggle=$('settings-toggle'), settingsPanel=$('settings-panel'), teamSizeSelect=$('team-size'), animationSpeedSelect=$('animation-speed'),
confirmModal=$('confirm-modal'), confirmModalContent=$('confirm-modal-content'), confirmModalMessage=$('confirm-modal-message'), confirmModalCancel=$('confirm-modal-cancel'), confirmModalOk=$('confirm-modal-ok'),
playerCharactersSelection=$('player-characters-selection'), characterSearchInput=$('character-search'), clearPlayerCharactersButton=$('clear-player-characters'),
saveTeamSection=$('save-characters-section'), teamNameInput=$('team-name-input'), saveTeamButton=$('save-team-button'),
saveWorldBossSection=$('save-world-boss-section'), saveWorldBossButton=$('save-world-boss-button'), saveWeeklyBossSection=$('save-weekly-boss-section'), saveWeeklyBossButton=$('save-weekly-boss-button'),
saveLegendSection=$('save-legend-section'), saveLegendButton=$('save-legend-button'), boomSound=$('boom-sound'),
charactersSection=$('characters-section'), worldBossesSection=$('world-bosses-section'), weeklyBossesSection=$('weekly-bosses-section'), localLegendsSection=$('local-legends-section'),
bossCategorySelect=$('boss-category-select'), bossesSettingsContainer=$('bosses-settings-container'),
fullHistoryModal=$('full-history-modal'), fullHistoryModalContent=$('full-history-modal-content'), fullHistoryModalContentInner=$('full-history-modal-content-inner'), fullHistoryModalCloseButton=$('full-history-modal-close');
/* ===== Музыка ===== */
const bgMusic = $('bg-music');
const musicToggle = $('music-toggle');
const volumeSlider = $('volume-slider');
const musicIconOn = $('music-icon-on');
const musicIconOff = $('music-icon-off');
let isMusicPlaying = false;
function initMusic() {
bgMusic.volume = 0.5;
volumeSlider.value = 50;
// Попытка автовоспроизведения
const playPromise = bgMusic.play();
if (playPromise !== undefined) {
playPromise.then(() => {
isMusicPlaying = true;
updateMusicIcon();
}).catch(() => {
// Браузер заблокировал автовоспроизведение
isMusicPlaying = false;
updateMusicIcon();
});
}
}
function updateMusicIcon() {
if (isMusicPlaying) {
musicIconOn.style.display = 'block';
musicIconOff.style.display = 'none';
} else {
musicIconOn.style.display = 'none';
musicIconOff.style.display = 'block';
}
}
musicToggle.addEventListener('click', () => {
if (isMusicPlaying) {
bgMusic.pause();
isMusicPlaying = false;
} else {
bgMusic.play().catch(() => {});
isMusicPlaying = true;
}
updateMusicIcon();
});
volumeSlider.addEventListener('input', (e) => {
bgMusic.volume = e.target.value / 100;
});
/* ===== Canvas пыль ===== */
function initDustParticles() {
const canvas = $('dust-canvas');
const ctx = canvas.getContext('2d');
let particles = [];
const particleCount = 50;
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', resize);
class Particle {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.size = Math.random() * 2 + 0.5;
this.speedX = (Math.random() - 0.5) * 0.3;
this.speedY = -Math.random() * 0.5 - 0.1;
this.opacity = Math.random() * 0.5 + 0.1;
this.color = Math.random() > 0.7 ? 'rgba(255,70,85,' : 'rgba(232,226,218,';
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.opacity -= 0.001;
if (this.y < -10 || this.opacity <= 0) {
this.reset();
this.y = canvas.height + 10;
this.opacity = Math.random() * 0.5 + 0.1;
}
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color + this.opacity + ')';
ctx.fill();
}
}
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
p.update();
p.draw();
});
requestAnimationFrame(animate);
}
animate();
}
/* ===== Эффекты Роновы: кресты, глаза, параллакс ===== */
function initRonovaFX() {
const fx = $('rv-fx'), pattern = $('rv-pattern'), eye = $('rv-eye');
if (!fx) return;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduced) {
// SVG глаза
const EYE_SVG = '<svg viewBox="0 0 100 50" fill="none" stroke="currentColor" stroke-width="4"><path d="M5 25 Q50 -5 95 25 Q50 55 5 25Z"/><circle cx="50" cy="25" r="14"/><path d="M50 15 l4 8 8 4 -8 4 -4 8 -4 -8 -8 -4 8 -4z" fill="currentColor" stroke="none"/></svg>';
// Создание 7 глаз по очереди с рандомными задержками
function spawnEyes() {
const eyeCount = 7;
const delays = [];
// Генерируем рандомные задержки для появления
for (let i = 0; i < eyeCount; i++) {
delays.push(i * 1200 + Math.random() * 800);
}
delays.forEach((delay, index) => {
setTimeout(() => {
const eyeEl = document.createElement('div');
eyeEl.className = 'rv-eye-instance';
// Рандомная позиция (избегая краёв)
const left = 10 + Math.random() * 80;
const top = 10 + Math.random() * 80;
const size = 40 + Math.random() * 40;
eyeEl.style.left = left + '%';
eyeEl.style.top = top + '%';
eyeEl.style.width = size + 'px';
eyeEl.style.height = (size / 2) + 'px';
eyeEl.style.color = 'var(--red-bright)';
eyeEl.style.filter = 'drop-shadow(0 0 8px rgba(255,70,85,.7))';
eyeEl.innerHTML = EYE_SVG;
fx.appendChild(eyeEl);
// Удаление после анимации
setTimeout(() => {
eyeEl.remove();
}, 8000);
}, delay);
});
}
// Запуск глаз
spawnEyes();
// Периодическое появление новых глаз
setInterval(() => {
if (Math.random() < 0.4) {
const eyeEl = document.createElement('div');
eyeEl.className = 'rv-eye-instance';
const left = 10 + Math.random() * 80;
const top = 10 + Math.random() * 80;
const size = 40 + Math.random() * 40;
eyeEl.style.left = left + '%';
eyeEl.style.top = top + '%';
eyeEl.style.width = size + 'px';
eyeEl.style.height = (size / 2) + 'px';
eyeEl.style.color = 'var(--red-bright)';
eyeEl.style.filter = 'drop-shadow(0 0 8px rgba(255,70,85,.7))';
eyeEl.innerHTML = EYE_SVG;
fx.appendChild(eyeEl);
setTimeout(() => eyeEl.remove(), 8000);
}
}, 5000);
// Кресты
const spawnCross = () => {
const el = document.createElement('div');
el.className = 'fx-cross';
el.textContent = '✕';
el.style.fontSize = (16 + Math.random() * 44) + 'px';
el.style.animationDuration = (3 + Math.random() * 2.5) + 's';
el.style.left = (4 + Math.random() * 92) + '%';
el.style.top = (6 + Math.random() * 88) + '%';
fx.appendChild(el);
setTimeout(() => el.remove(), 7000);
};
setInterval(() => { if (Math.random() < 0.6) spawnCross(); }, 4000);
}
// Параллакс
let mx = 0, my = 0, sy = window.scrollY;
const apply = () => {
if (eye) eye.style.transform = `translate(${mx * 14}px, ${my * 10 + sy * 0.04}px)`;
if (pattern) pattern.style.transform = `translate(${mx * -8}px, ${my * -6 + sy * 0.05}px)`;
};
window.addEventListener('mousemove', e => { mx = e.clientX / window.innerWidth - .5; my = e.clientY / window.innerHeight - .5; apply(); });
window.addEventListener('scroll', () => { sy = window.scrollY; apply(); }, { passive: true });
}
function initTheme() {
document.body.classList.remove('light', 'dark');
document.body.classList.add(config.darkMode ? 'dark' : 'light');
}
function toggleTheme() {
config.darkMode = !config.darkMode;
initTheme();
localStorage.setItem('theme', config.darkMode ? 'dark' : 'light');
}
function toggleSettings() { settingsPanel.classList.toggle('open'); }
function changeCategory() {
config.category = categorySelect.value;
localStorage.setItem('genshinCategory', config.category);
[charactersSection, worldBossesSection, weeklyBossesSection, localLegendsSection].forEach(s => s.classList.add('hidden'));
({ 'characters': charactersSection, 'world-bosses': worldBossesSection, 'weekly-bosses': weeklyBossesSection, 'local-legends': localLegendsSection })[config.category].classList.remove('hidden');
}
function createCharacterCard(character) {
const card = document.createElement('div');
card.className = 'char-card';
card.innerHTML = `
<img src="${character.icon}" alt="${character.name}"
onerror="this.onerror=null;this.src='https://placehold.co/96x96/14141E/FF4655?text=N/A'">
<p>${character.name}</p>`;
return card;
}
function createBossCard(boss) {
const card = document.createElement('div');
card.className = 'boss-card';
card.innerHTML = `
<img src="${boss.icon}" alt="${boss.name}" onerror="this.onerror=null;this.src='https://placehold.co/90x90/14141E/FF4655?text=N/A'">
<h3>${boss.name}</h3>`;
return card;
}
function createLegendCard(legend) {
const card = document.createElement('div');
card.className = 'boss-card legend-card';
const isClickable = legend.guideUrl && legend.guideUrl !== "#";
card.innerHTML = `
<img src="${legend.icon}" alt="${legend.name}" onerror="this.onerror=null;this.src='https://placehold.co/90x90/14141E/FF4655?text=N/A'">
<h3>${legend.name}</h3>
<p>${isClickable ? 'Нажмите чтобы узнать про Легенду' : 'Нет ссылки на гайд'}</p>`;
if (isClickable) { card.style.cursor = 'pointer'; card.addEventListener('click', () => window.open(legend.guideUrl, '_blank')); }
return card;
}
function createPlayerCharacterSelectionCard(character) {
const card = document.createElement('div');
card.className = 'player-char-card flex flex-col items-center p-2 text-center cursor-pointer relative';
card.setAttribute('data-name', character.name);
const isSelected = playerCharacters.some(c => c.name === character.name);
if (isSelected) card.classList.add('selected');
const elementIconHtml = character.element ?
`<img src="pic/Elements/Element_${character.element}.webp" alt="${character.element}" class="w-5 h-5 absolute top-1 right-1" title="${character.element}">` : '';
card.innerHTML = `
<div class="relative">
<img src="${character.icon}" alt="${character.name}" class="w-14 h-14 rounded-full mb-2 object-cover"
style="border:2px solid ${isSelected ? 'var(--red-bright)' : 'transparent'}"
onerror="this.onerror=null;this.src='https://placehold.co/56x56/14141E/FF4655?text=N/A'">
${elementIconHtml}
</div>
<p class="font-medium text-xs truncate w-full" style="color:var(--bone)">${character.name}</p>
${isSelected ? '<button class="remove-btn" title="Удалить">&times;</button>' : ''}`;
card.addEventListener('click', (event) => {
if (event.target.classList.contains('remove-btn')) removePlayerCharacter(character.name);
else togglePlayerCharacter(character);
});
return card;
}
function renderBossSettings() {
const category = bossCategorySelect.value;
const bosses = category === 'world-bosses' ? WORLD_BOSSES : category === 'weekly-bosses' ? WEEKLY_BOSSES : LOCAL_LEGENDS;
bossesSettingsContainer.innerHTML = '';
bosses.forEach(boss => {
const isExcluded = config.excludedBosses[category].includes(boss.name);
const bossToggle = document.createElement('div');
bossToggle.className = 'boss-toggle flex items-center';
const id = `boss-${category}-${boss.name.replace(/\s+/g, '-')}`;
bossToggle.innerHTML = `
<input type="checkbox" id="${id}" ${isExcluded ? '' : 'checked'} class="boss-toggle-checkbox">
<label for="${id}" class="ml-2 font-medium" style="color:var(--bone)">${boss.name}</label>`;
bossesSettingsContainer.appendChild(bossToggle);
bossToggle.querySelector('.boss-toggle-checkbox').addEventListener('change', (e) => {
const list = config.excludedBosses[category];
if (e.target.checked) { const i = list.indexOf(boss.name); if (i > -1) list.splice(i, 1); }
else if (!list.includes(boss.name)) list.push(boss.name);
localStorage.setItem(`${currentUserUID}_excludedBosses`, JSON.stringify(config.excludedBosses));
});
});
}
function renderPlayerCharactersSelection(filter = '') {
playerCharactersSelection.innerHTML = '';
allCharacters.filter(char => char.name.toLowerCase().includes(filter.toLowerCase()))
.forEach(char => playerCharactersSelection.appendChild(createPlayerCharacterSelectionCard(char)));
updateClearPlayerCharactersButtonVisibility();
}
function togglePlayerCharacter(character) {
const index = playerCharacters.findIndex(c => c.name === character.name);
if (index > -1) playerCharacters.splice(index, 1); else playerCharacters.push(character);
localStorage.setItem(`${currentUserUID}_playerCharacters`, JSON.stringify(playerCharacters));
renderPlayerCharactersSelection(characterSearchInput.value);
}
function removePlayerCharacter(name) {
playerCharacters = playerCharacters.filter(c => c.name !== name);
localStorage.setItem(`${currentUserUID}_playerCharacters`, JSON.stringify(playerCharacters));
renderPlayerCharactersSelection(characterSearchInput.value);
}
function clearPlayerCharacters() {
playerCharacters = [];
localStorage.removeItem(`${currentUserUID}_playerCharacters`);
renderPlayerCharactersSelection('');
hideConfirmModal();
}
function updateClearPlayerCharactersButtonVisibility() {
clearPlayerCharactersButton.classList.toggle('hidden', playerCharacters.length === 0);
}
async function renderCharactersResult(team) {
charactersResult.innerHTML = '';
charactersPlaceholder.classList.add('hidden');
saveTeamSection.classList.add('hidden');
for (const char of team) {
const card = createCharacterCard(char);
card.classList.add('roulette-item-boom-enter');
charactersResult.appendChild(card);
if (boomSound) { boomSound.currentTime = 0; boomSound.play().catch(() => {}); }
await new Promise(r => setTimeout(r, 100));
card.classList.add('roulette-item-boom-enter-active');
await new Promise(r => setTimeout(r, 500));
}
saveTeamSection.classList.remove('hidden');
teamNameInput.value = '';
currentGeneratedTeam = team;
}
async function renderBossResult(boss, isWorld) {
const resultDiv = isWorld ? worldBossResult : weeklyBossResult;
const placeholder = isWorld ? worldBossPlaceholder : weeklyBossPlaceholder;
const saveSection = isWorld ? saveWorldBossSection : saveWeeklyBossSection;
resultDiv.innerHTML = ''; placeholder.classList.add('hidden'); saveSection.classList.add('hidden');
const card = createBossCard(boss);
card.classList.add('roulette-item-boom-enter');
resultDiv.appendChild(card);
if (boomSound) { boomSound.currentTime = 0; boomSound.play().catch(() => {}); }
await new Promise(r => setTimeout(r, 100));
card.classList.add('roulette-item-boom-enter-active');
await new Promise(r => setTimeout(r, 500));
saveSection.classList.remove('hidden');
currentGeneratedBoss = boss; isWorldBoss = isWorld; isLegend = false;
}
async function renderLegendResult(legend) {
legendResult.innerHTML = ''; legendPlaceholder.classList.add('hidden'); saveLegendSection.classList.add('hidden');
const card = createLegendCard(legend);
card.classList.add('roulette-item-boom-enter');
legendResult.appendChild(card);
if (boomSound) { boomSound.currentTime = 0; boomSound.play().catch(() => {}); }
await new Promise(r => setTimeout(r, 100));
card.classList.add('roulette-item-boom-enter-active');
await new Promise(r => setTimeout(r, 500));
saveLegendSection.classList.remove('hidden');
currentGeneratedBoss = legend; isWorldBoss = false; isLegend = true;
}
function historyTitle(item) {
if (item.type === 'team') { const n = item.name ? `"${item.name}" - ` : ''; return `${n}Отряд (${new Date(item.date).toLocaleString()})`; }
if (item.type === 'legend') return `${item.name} (Местная легенда) (${new Date(item.date).toLocaleString()})`;
return `${item.name} (${new Date(item.date).toLocaleString()})`;
}
function renderHistory(limit = 3) {
historyContent.innerHTML = '';
const allHistory = [...historyTeams, ...historyBosses].sort((a, b) => new Date(b.date) - new Date(a.date));
if (allHistory.length === 0) {
historyPlaceholder.classList.remove('hidden');
clearHistoryButton.classList.add('hidden');
showFullHistoryButton.classList.add('hidden');
return;
}
historyPlaceholder.classList.add('hidden');
clearHistoryButton.classList.remove('hidden');
showFullHistoryButton.classList.remove('hidden');
allHistory.slice(0, limit).forEach((item) => {
const el = document.createElement('div');
el.className = `result-panel cornered p-5 ${item.type === 'team' ? 'history-item' : 'boss-history-item'}`;
const title = document.createElement('h3');
title.className = 'text-xl font-bold mb-4 rv-label';
title.textContent = historyTitle(item);
el.appendChild(title);
const container = document.createElement('div');
container.className = 'flex flex-wrap justify-center gap-4';
if (item.type === 'team') item.characters.forEach(c => container.appendChild(createCharacterCard(c)));
else if (item.type === 'legend') container.appendChild(createLegendCard(item));
else container.appendChild(createBossCard(item));
el.appendChild(container);
historyContent.appendChild(el);
});
}
function fullHistoryCard(img, name, extra = '') {
const card = document.createElement('div');
card.className = 'full-history-character-card';
card.innerHTML = `
<img src="${img}" alt="${name}" onerror="this.onerror=null;this.src='https://placehold.co/64x64/14141E/FF4655?text=N/A'">
<p>${name}</p>${extra}`;
return card;
}
function showFullHistoryModal() {
fullHistoryModalContentInner.innerHTML = '';
const allHistory = [...historyTeams, ...historyBosses].sort((a, b) => new Date(b.date) - new Date(a.date));
if (allHistory.length === 0) fullHistoryModalContentInner.innerHTML = '<p class="text-center rv-sub">История пуста.</p>';
else allHistory.forEach((item) => {
const el = document.createElement('div');
el.className = `result-panel cornered p-5 ${item.type === 'team' ? 'history-item' : 'boss-history-item'}`;
const title = document.createElement('h3');
title.className = 'text-xl font-bold mb-4 rv-label';
title.textContent = historyTitle(item);
el.appendChild(title);
const container = document.createElement('div');
container.className = 'flex gap-4 overflow-x-auto pb-3 custom-scrollbar';
if (item.type === 'team') item.characters.forEach(c => container.appendChild(fullHistoryCard(c.icon, c.name)));
else if (item.type === 'legend') container.appendChild(fullHistoryCard(item.icon, item.name, '<p class="text-xs mt-1 rv-sub">Местная легенда</p>'));
else container.appendChild(fullHistoryCard(item.icon, item.name));
el.appendChild(container);
fullHistoryModalContentInner.appendChild(el);
});
fullHistoryModal.classList.remove('hidden');
setTimeout(() => { fullHistoryModalContent.classList.remove('scale-95', 'opacity-0'); fullHistoryModalContent.classList.add('scale-100', 'opacity-100'); }, 10);
}
function hideFullHistoryModal() {
fullHistoryModalContent.classList.add('scale-95', 'opacity-0');
fullHistoryModalContent.classList.remove('scale-100', 'opacity-100');
setTimeout(() => fullHistoryModal.classList.add('hidden'), 300);
}
function saveTeamToHistory(team, name = '') {
historyTeams.unshift({ date: new Date().toISOString(), characters: team, name, type: 'team' });
localStorage.setItem(`${currentUserUID}_genshinTeamsHistory`, JSON.stringify(historyTeams));
renderHistory();
}
function saveBossToHistory(boss, isWorld, isLegendType = false) {
historyBosses.unshift({ date: new Date().toISOString(), name: boss.name, icon: boss.icon, type: isLegendType ? 'legend' : (isWorld ? 'world-boss' : 'weekly-boss') });
localStorage.setItem(`${currentUserUID}_genshinBossesHistory`, JSON.stringify(historyBosses));
renderHistory();
}
function clearHistory() {
historyTeams = []; historyBosses = [];
localStorage.removeItem(`${currentUserUID}_genshinTeamsHistory`);
localStorage.removeItem(`${currentUserUID}_genshinBossesHistory`);
renderHistory(); hideConfirmModal();
}
async function spinCharacters() {
const pool = playerCharacters.length > 0 ? playerCharacters : allCharacters;
if (isSpinning || pool.length < config.teamSize) { alert(`Недостаточно персонажей для отряда из ${config.teamSize}. Доступно: ${pool.length}`); return; }
isSpinning = true; spinCharactersButton.disabled = true; spinCharactersButton.textContent = 'Крутим...';
charactersResult.innerHTML = ''; charactersPlaceholder.classList.add('hidden'); charactersLoader.classList.remove('hidden'); saveTeamSection.classList.add('hidden');
const teamSize = parseInt(teamSizeSelect.value);
let charsPool = [...pool]; const selectedTeam = [];
const spinDurationMs = animationSpeedSelect.value === 'fast' ? 1500 : animationSpeedSelect.value === 'slow' ? 4000 : 2500;
const individualSpinTime = spinDurationMs / teamSize;
for (let i = 0; i < teamSize; i++) {
const tempDiv = document.createElement('div');
tempDiv.className = 'char-slot';
charactersResult.appendChild(tempDiv);
const startTime = Date.now(); let animationFrame;
const animateSpin = () => {
if (Date.now() - startTime < individualSpinTime) {
const tempCard = createCharacterCard(charsPool[Math.floor(Math.random() * charsPool.length)]);
tempCard.classList.add('roulette-item-spin');
tempDiv.innerHTML = ''; tempDiv.appendChild(tempCard);
animationFrame = requestAnimationFrame(animateSpin);
} else {
cancelAnimationFrame(animationFrame);
selectedTeam.push(charsPool.splice(Math.floor(Math.random() * charsPool.length), 1)[0]);
tempDiv.innerHTML = '';
}
};
animateSpin();
await new Promise(r => setTimeout(r, individualSpinTime + 100));
}
charactersLoader.classList.add('hidden');
spinCharactersButton.disabled = false; spinCharactersButton.textContent = 'Крутить ещё!';
isSpinning = false;
renderCharactersResult(selectedTeam);
}
async function spinBossGeneric(kind) {
if (isSpinning) return;
const data = kind === 'world' ? { list: WORLD_BOSSES, btn: spinWorldBossButton, res: worldBossResult, ph: worldBossPlaceholder, ld: worldBossLoader, sv: saveWorldBossSection, key: 'world-bosses' }
: kind === 'weekly' ? { list: WEEKLY_BOSSES, btn: spinWeeklyBossButton, res: weeklyBossResult, ph: weeklyBossPlaceholder, ld: weeklyBossLoader, sv: saveWeeklyBossSection, key: 'weekly-bosses' }
: { list: LOCAL_LEGENDS, btn: spinLegendButton, res: legendResult, ph: legendPlaceholder, ld: legendLoader, sv: saveLegendSection, key: 'local-legends' };
isSpinning = true; data.btn.disabled = true; data.btn.textContent = 'Выбираем...';
data.res.innerHTML = ''; data.ph.classList.add('hidden'); data.ld.classList.remove('hidden'); data.sv.classList.add('hidden');
await new Promise(r => setTimeout(r, 1000));
const available = data.list.filter(b => !config.excludedBosses[data.key].includes(b.name));
if (available.length === 0) {
data.res.innerHTML = '<p class="text-center" style="color:var(--red-bright)">Нет доступных целей! Проверьте настройки.</p>';
data.ld.classList.add('hidden'); data.btn.disabled = false; data.btn.textContent = 'Выбрать ещё!'; isSpinning = false; return;
}
const pick = available[Math.floor(Math.random() * available.length)];
data.ld.classList.add('hidden'); data.btn.disabled = false; data.btn.textContent = 'Выбрать ещё!'; isSpinning = false;
if (kind === 'world') renderBossResult(pick, true);
else if (kind === 'weekly') renderBossResult(pick, false);
else renderLegendResult(pick);
}
function showConfirmModal(message, callback) {
confirmModalMessage.textContent = message;
confirmModal.classList.remove('hidden');
setTimeout(() => { confirmModalContent.classList.remove('scale-95', 'opacity-0'); confirmModalContent.classList.add('scale-100', 'opacity-100'); }, 10);
const onOk = () => { cleanup(); callback(); };
const onCancel = () => { cleanup(); hideConfirmModal(); };
const cleanup = () => { confirmModalOk.removeEventListener('click', onOk); confirmModalCancel.removeEventListener('click', onCancel); };
confirmModalOk.addEventListener('click', onOk);
confirmModalCancel.addEventListener('click', onCancel);
}
function hideConfirmModal() {
confirmModalContent.classList.add('scale-95', 'opacity-0');
confirmModalContent.classList.remove('scale-100', 'opacity-100');
setTimeout(() => confirmModal.classList.add('hidden'), 300);
}
themeToggle.addEventListener('click', toggleTheme);
settingsToggle.addEventListener('click', toggleSettings);
categorySelect.addEventListener('change', changeCategory);
bossCategorySelect.addEventListener('change', renderBossSettings);
spinCharactersButton.addEventListener('click', spinCharacters);
spinWorldBossButton.addEventListener('click', () => spinBossGeneric('world'));
spinWeeklyBossButton.addEventListener('click', () => spinBossGeneric('weekly'));
spinLegendButton.addEventListener('click', () => spinBossGeneric('legend'));
clearHistoryButton.addEventListener('click', () => showConfirmModal('Вы уверены, что хотите очистить всю историю?', clearHistory));
clearPlayerCharactersButton.addEventListener('click', () => showConfirmModal('Вы уверены, что хотите очистить список ваших персонажей?', clearPlayerCharacters));
showFullHistoryButton.addEventListener('click', showFullHistoryModal);
fullHistoryModalCloseButton.addEventListener('click', hideFullHistoryModal);
fullHistoryModal.addEventListener('click', e => { if (e.target === fullHistoryModal) hideFullHistoryModal(); });
confirmModal.addEventListener('click', e => { if (e.target === confirmModal) hideConfirmModal(); });
characterSearchInput.addEventListener('input', e => renderPlayerCharactersSelection(e.target.value));
saveTeamButton.addEventListener('click', () => { if (currentGeneratedTeam.length) { saveTeamToHistory(currentGeneratedTeam, teamNameInput.value.trim()); saveTeamSection.classList.add('hidden'); } });
saveWorldBossButton.addEventListener('click', () => { if (currentGeneratedBoss) { saveBossToHistory(currentGeneratedBoss, true); saveWorldBossSection.classList.add('hidden'); } });
saveWeeklyBossButton.addEventListener('click', () => { if (currentGeneratedBoss) { saveBossToHistory(currentGeneratedBoss, false); saveWeeklyBossSection.classList.add('hidden'); } });
saveLegendButton.addEventListener('click', () => { if (currentGeneratedBoss) { saveBossToHistory(currentGeneratedBoss, false, true); saveLegendSection.classList.add('hidden'); } });
teamSizeSelect.addEventListener('change', e => { config.teamSize = parseInt(e.target.value); localStorage.setItem(`${currentUserUID}_teamSize`, config.teamSize); });
animationSpeedSelect.addEventListener('change', e => { config.animationSpeed = e.target.value; localStorage.setItem(`${currentUserUID}_animationSpeed`, config.animationSpeed); });
teamSizeSelect.value = config.teamSize;
animationSpeedSelect.value = config.animationSpeed;
categorySelect.value = config.category;
bossCategorySelect.value = 'world-bosses';
initTheme();
initMusic();
initDustParticles();
initRonovaFX();
renderPlayerCharactersSelection();
renderHistory();
renderBossSettings();
changeCategory();
});
</script>
</body>
</html>