GG / index.html
Mycc's picture
Update index.html
89d9611 verified
Raw
History Blame Contribute Delete
197 kB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Random Genshin Impact</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<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=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
--primary: #667eea;
--secondary: #764ba2;
--accent: #f093fb;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--dark: #1e293b;
--light: #f8fafc;
}
body {
font-family: 'Inter', sans-serif;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
min-height: 100vh;
color: #e2e8f0;
}
.glass {
background: rgba(30, 41, 59, 0.85);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.card-gradient {
background: linear-gradient(145deg, #1e293b, #0f172a);
border-radius: 20px;
box-shadow: 10px 10px 30px #0a0f1a, -10px -10px 30px #2a3a52;
}
.gradient-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.gradient-secondary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.gradient-accent {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.gradient-gold {
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
}
.gradient-purple {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
.gradient-success {
background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
}
.gradient-warning {
background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%);
}
.gradient-danger {
background: linear-gradient(135deg, #ef4444 0%, #f87171 100%);
}
.loader {
width: 60px;
height: 60px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-radius: 50%;
border-top-color: var(--primary);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.float-animation {
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-15px) rotate(5deg); }
}
.card-hover {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-10px) scale(1.02);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
}
.settings-panel {
max-height: 0;
overflow: hidden;
transition: max-height 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.settings-panel.open {
max-height: 3000px;
}
.roulette-spin {
animation: roulette-spin 0.1s linear infinite;
}
@keyframes roulette-spin {
0% { transform: rotate(0deg) scale(1); }
50% { transform: rotate(180deg) scale(1.1); }
100% { transform: rotate(360deg) scale(1); }
}
.reveal-animation {
animation: reveal 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55) forwards;
}
@keyframes reveal {
0% {
opacity: 0;
transform: scale(0.3) rotate(-15deg);
filter: blur(10px);
}
70% {
opacity: 1;
transform: scale(1.1) rotate(5deg);
filter: blur(0);
}
100% {
opacity: 1;
transform: scale(1) rotate(0);
filter: blur(0);
}
}
.element-icon {
position: absolute;
bottom: -8px;
right: -8px;
width: 28px;
height: 28px;
border-radius: 50%;
background: #1e293b;
padding: 3px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.tab-active {
position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
.glow {
box-shadow: 0 0 35px rgba(102, 126, 234, 0.6);
}
.particle {
position: absolute;
width: 6px;
height: 6px;
background: #667eea;
border-radius: 50%;
animation: particle-float 2s ease-out forwards;
z-index: 1;
}
@keyframes particle-float {
0% {
transform: translate(0, 0) scale(1);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty)) scale(0);
opacity: 0;
}
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.slide-in {
animation: slide-in 0.5s ease-out forwards;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.progress-bar {
height: 6px;
border-radius: 3px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
width: 0%;
transition: width 0.3s ease;
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.8);
}
.character-card {
position: relative;
overflow: hidden;
}
.character-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
transform: scaleX(0);
transition: transform 0.3s ease;
transform-origin: left;
}
.character-card:hover::before {
transform: scaleX(1);
}
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
.shine {
position: relative;
overflow: hidden;
}
.shine::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: rotate(30deg);
transition: transform 0.6s;
}
.shine:hover::after {
transform: rotate(30deg) translate(20%, 20%);
}
.stats-card {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border-left: 4px solid #667eea;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-team {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.badge-boss {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.badge-legend {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
color: white;
}
.input-field {
transition: all 0.3s ease;
background: rgba(30, 41, 59, 0.9);
border: 2px solid transparent;
}
.input-field:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
outline: none;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 30px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 22px;
width: 22px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
input:checked + .toggle-slider:before {
transform: translateX(30px);
}
.character-avatar {
transition: all 0.3s ease;
position: relative;
}
.character-avatar.selected {
transform: scale(1.05);
box-shadow: 0 0 0 3px #667eea, 0 10px 25px rgba(102, 126, 234, 0.3);
}
.modal-backdrop {
animation: fadeIn 0.3s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(50px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.tooltip {
position: relative;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
color: #1e293b;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.875rem;
white-space: nowrap;
z-index: 100;
margin-bottom: 5px;
}
.tooltip:hover::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: rgba(255, 255, 255, 0.9);
margin-bottom: -5px;
z-index: 100;
}
.grid-stack {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 1.25rem;
}
@media (min-width: 640px) {
.grid-stack {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
}
@media (min-width: 768px) {
.grid-stack {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
}
@media (min-width: 1024px) {
.grid-stack {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
.result-container {
min-height: 350px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.result-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border-radius: 20px;
}
.history-item {
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.history-item.team {
border-left-color: #667eea;
}
.history-item.world-boss {
border-left-color: #4facfe;
}
.history-item.weekly-boss {
border-left-color: #f6d365;
}
.history-item.legend {
border-left-color: #a8edea;
}
.history-item:hover {
transform: translateX(5px);
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
.stagger-item {
opacity: 0;
transform: translateY(20px);
}
.stagger-item.visible {
animation: slide-in 0.5s ease-out forwards;
}
.confetti {
position: fixed;
width: 15px;
height: 15px;
background: var(--bg);
top: -10px;
opacity: 0;
z-index: 1000;
}
@keyframes confetti-fall {
0% {
opacity: 1;
transform: translateY(0) rotate(0deg);
}
100% {
opacity: 0;
transform: translateY(100vh) rotate(720deg);
}
}
.star-rating {
color: #fbbf24;
}
.star-rating .empty {
color: #4b5563;
}
.progress-ring {
transform: rotate(-90deg);
}
.progress-ring-circle {
stroke-dasharray: 283;
stroke-dashoffset: 283;
transition: stroke-dashoffset 0.5s ease;
}
/* Улучшенные пропорции */
.card-padding {
padding: 1.75rem;
}
.section-padding {
padding: 3rem;
}
@media (max-width: 768px) {
.section-padding {
padding: 1.5rem;
}
.card-padding {
padding: 1.25rem;
}
}
.header-height {
min-height: auto;
padding: 2rem 0;
}
.stat-card {
padding: 1.5rem;
border-radius: 1.25rem;
}
.character-avatar-lg {
width: 110px;
height: 110px;
}
.character-avatar-md {
width: 90px;
height: 90px;
}
.boss-avatar {
width: 140px;
height: 140px;
}
.button-padding {
padding: 1rem 2.5rem;
}
.tab-button {
padding: 0.875rem 2rem;
border-radius: 1.25rem;
}
</style>
</head>
<body>
<div id="app-container" class="container mx-auto px-4 py-6 md:py-8 max-w-7xl">
<!-- Header -->
<header class="text-center mb-10">
<div class="flex flex-col md:flex-row justify-between items-center mb-8 gap-6">
<!-- Left controls -->
<div class="flex items-center space-x-4">
<button id="language-toggle" class="p-3 rounded-2xl glass hover:scale-105 transition-transform duration-300 group tooltip" data-tooltip="Switch language">
<div class="flex flex-col items-center">
<i class="fas fa-language text-blue-300 text-xl"></i>
<span id="language-label" class="text-xs font-medium mt-1">EN</span>
</div>
</button>
<div class="hidden md:flex items-center space-x-2 px-4 py-2 rounded-2xl glass">
<i class="fas fa-user text-purple-400"></i>
<span class="text-sm font-medium text-gray-300">UID:</span>
<span id="user-uid" class="text-sm font-mono bg-gray-800 px-2 py-1 rounded-lg"></span>
</div>
</div>
<!-- Center title -->
<div class="text-center order-first md:order-none">
<div class="flex items-center justify-center mb-3">
<div class="w-12 h-12 rounded-full gradient-primary flex items-center justify-center mr-3">
<i class="fas fa-dice text-white text-xl"></i>
</div>
<h1 class="text-4xl md:text-5xl lg:text-5xl font-bold bg-gradient-to-r from-purple-500 via-blue-400 to-purple-500 bg-clip-text text-transparent animate-pulse">
<span data-i18n="title">Genshin Roulette</span>
</h1>
</div>
<p class="text-lg text-gray-300 max-w-2xl mx-auto" data-i18n="subtitle">
Random characters, bosses & legends for your Genshin Impact adventure
</p>
</div>
<!-- Right controls -->
<div class="flex items-center space-x-4">
<button id="stats-toggle" class="p-3 rounded-2xl glass hover:scale-105 transition-transform duration-300 group tooltip" data-tooltip="Show stats">
<i class="fas fa-chart-bar text-green-300 text-xl"></i>
</button>
<button id="settings-toggle" class="p-3 rounded-2xl glass hover:scale-105 transition-transform duration-300 group tooltip" data-tooltip="Settings">
<i class="fas fa-cog text-gray-300 group-hover:rotate-90 transition-transform text-xl"></i>
</button>
<button id="help-toggle" class="p-3 rounded-2xl glass hover:scale-105 transition-transform duration-300 group tooltip" data-tooltip="Help">
<i class="fas fa-question text-yellow-300 text-xl"></i>
</button>
</div>
</div>
<!-- Stats Panel -->
<div id="stats-panel" class="settings-panel glass rounded-3xl p-6 mb-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="stats-card p-4 rounded-2xl">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-400" data-i18n="stats.characters">Characters</p>
<p class="text-2xl font-bold text-white" id="total-characters">120</p>
</div>
<i class="fas fa-users text-purple-400 text-2xl"></i>
</div>
<div class="mt-2 text-xs text-gray-400">
<span data-i18n="stats.owned">Owned:</span> <span id="owned-characters">0</span>
</div>
</div>
<div class="stats-card p-4 rounded-2xl">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-400" data-i18n="stats.worldBosses">World Bosses</p>
<p class="text-2xl font-bold text-white" id="total-world-bosses">40</p>
</div>
<i class="fas fa-dragon text-blue-400 text-2xl"></i>
</div>
<div class="mt-2 text-xs text-gray-400">
<span data-i18n="stats.available">Available:</span> <span id="available-world-bosses">40</span>
</div>
</div>
<div class="stats-card p-4 rounded-2xl">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-400" data-i18n="stats.weeklyBosses">Weekly Bosses</p>
<p class="text-2xl font-bold text-white" id="total-weekly-bosses">12</p>
</div>
<i class="fas fa-crown text-yellow-400 text-2xl"></i>
</div>
<div class="mt-2 text-xs text-gray-400">
<span data-i18n="stats.available">Available:</span> <span id="available-weekly-bosses">12</span>
</div>
</div>
<div class="stats-card p-4 rounded-2xl">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-400" data-i18n="stats.legends">Legends</p>
<p class="text-2xl font-bold text-white" id="total-legends">30</p>
</div>
<i class="fas fa-landmark text-pink-400 text-2xl"></i>
</div>
<div class="mt-2 text-xs text-gray-400">
<span data-i18n="stats.available">Available:</span> <span id="available-legends">30</span>
</div>
</div>
</div>
</div>
<!-- Category Tabs -->
<div class="flex flex-wrap justify-center gap-3 mb-8">
<button data-category="characters" class="tab-btn px-6 py-3 rounded-2xl glass font-semibold transition-all duration-300 hover:scale-105 flex items-center group tab-button">
<i class="fas fa-users mr-2 text-purple-400 group-hover:text-white transition-colors"></i>
<span data-i18n="tab.characters">Character Team</span>
</button>
<button data-category="world-bosses" class="tab-btn px-6 py-3 rounded-2xl glass font-semibold transition-all duration-300 hover:scale-105 flex items-center group tab-button">
<i class="fas fa-dragon mr-2 text-blue-400 group-hover:text-white transition-colors"></i>
<span data-i18n="tab.worldBosses">World Bosses</span>
</button>
<button data-category="weekly-bosses" class="tab-btn px-6 py-3 rounded-2xl glass font-semibold transition-all duration-300 hover:scale-105 flex items-center group tab-button">
<i class="fas fa-crown mr-2 text-yellow-400 group-hover:text-white transition-colors"></i>
<span data-i18n="tab.weeklyBosses">Weekly Bosses</span>
</button>
<button data-category="local-legends" class="tab-btn px-6 py-3 rounded-2xl glass font-semibold transition-all duration-300 hover:scale-105 flex items-center group tab-button">
<i class="fas fa-landmark mr-2 text-pink-400 group-hover:text-white transition-colors"></i>
<span data-i18n="tab.localLegends">Local Legends</span>
</button>
</div>
</header>
<main>
<!-- Settings Panel -->
<div id="settings-panel" class="settings-panel glass rounded-3xl p-8 mb-10">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
<h3 class="text-2xl font-bold text-white flex items-center">
<i class="fas fa-sliders-h mr-3 text-purple-400"></i>
<span data-i18n="settings.title">Roulette Settings</span>
</h3>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2">
<span class="text-sm font-medium text-gray-300" data-i18n="settings.sound">Sound:</span>
<label class="toggle-switch">
<input type="checkbox" id="sound-toggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm font-medium text-gray-300" data-i18n="settings.animations">Animations:</span>
<label class="toggle-switch">
<input type="checkbox" id="animations-toggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Basic Settings -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
<div class="card-gradient p-6 rounded-2xl">
<label for="team-size" class="block text-sm font-semibold mb-3 text-gray-300 flex items-center">
<i class="fas fa-user-group mr-2 text-purple-400"></i>
<span data-i18n="settings.teamSize">Team Size</span>
</label>
<select id="team-size" class="w-full p-3 rounded-xl input-field focus:ring-2 focus:ring-purple-500 transition-all">
<option value="4">4</option>
<option value="3">3</option>
<option value="2">2</option>
<option value="1">1</option>
</select>
</div>
<div class="card-gradient p-6 rounded-2xl">
<label for="animation-speed" class="block text-sm font-semibold mb-3 text-gray-300 flex items-center">
<i class="fas fa-tachometer-alt mr-2 text-blue-400"></i>
<span data-i18n="settings.animationSpeed">Animation Speed</span>
</label>
<select id="animation-speed" class="w-full p-3 rounded-xl input-field focus:ring-2 focus:ring-purple-500 transition-all">
<option value="fast" data-i18n="settings.speed.fast">Fast</option>
<option value="medium" data-i18n="settings.speed.medium" selected>Medium</option>
<option value="slow" data-i18n="settings.speed.slow">Slow</option>
</select>
</div>
<div class="card-gradient p-6 rounded-2xl">
<label for="duplicates" class="block text-sm font-semibold mb-3 text-gray-300 flex items-center">
<i class="fas fa-copy mr-2 text-green-400"></i>
<span data-i18n="settings.duplicates">Allow Duplicates</span>
</label>
<select id="duplicates" class="w-full p-3 rounded-xl input-field focus:ring-2 focus:ring-purple-500 transition-all">
<option value="no" data-i18n="settings.duplicates.no">No Duplicates</option>
<option value="yes" data-i18n="settings.duplicates.yes">Allow Duplicates</option>
</select>
</div>
</div>
<!-- My Characters Section -->
<div class="mb-10">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<h3 class="text-2xl font-bold text-white flex items-center">
<i class="fas fa-user-check mr-3 text-green-400"></i>
<span data-i18n="settings.myCharacters">My Characters</span>
</h3>
<div class="flex items-center space-x-3">
<div class="px-4 py-2 rounded-xl glass">
<span class="text-sm font-medium text-gray-300">
<span data-i18n="settings.selected">Selected:</span>
<span id="selected-count">0</span>/<span id="total-characters-count">120</span>
</span>
</div>
<button id="select-all-characters" class="px-4 py-2 rounded-xl gradient-success text-white font-semibold hover:scale-105 transition-transform">
<span data-i18n="settings.selectAll">Select All</span>
</button>
<button id="clear-player-characters" class="px-4 py-2 rounded-xl gradient-danger text-white font-semibold hover:scale-105 transition-transform hidden">
<span data-i18n="settings.clearSelected">Clear All</span>
</button>
</div>
</div>
<p class="text-gray-400 mb-6" data-i18n="settings.myCharactersDesc">
Select characters you own. If none selected, all characters will be used.
</p>
<!-- Search and Filters -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="md:col-span-2">
<div class="relative">
<i class="fas fa-search absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
<input type="text" id="character-search"
placeholder="Search characters by name..."
data-i18n-placeholder="settings.searchPlaceholder"
class="w-full pl-12 pr-4 py-3 rounded-2xl input-field focus:ring-2 focus:ring-purple-500 transition-all">
</div>
</div>
<div>
<select id="element-filter" class="w-full p-3 rounded-xl input-field focus:ring-2 focus:ring-purple-500 transition-all">
<option value="all" data-i18n="filters.allElements">All Elements</option>
<option value="Pyro" data-i18n="elements.pyro">Pyro</option>
<option value="Hydro" data-i18n="elements.hydro">Hydro</option>
<option value="Anemo" data-i18n="elements.anemo">Anemo</option>
<option value="Electro" data-i18n="elements.electro">Electro</option>
<option value="Dendro" data-i18n="elements.dendro">Dendro</option>
<option value="Cryo" data-i18n="elements.cryo">Cryo</option>
<option value="Geo" data-i18n="elements.geo">Geo</option>
</select>
</div>
</div>
<!-- Character Selection Grid -->
<div id="player-characters-selection" class="grid-stack mb-6 max-h-[500px] overflow-y-auto p-2 custom-scrollbar">
<!-- Characters will be dynamically added here -->
</div>
<!-- Progress Bar -->
<div class="mb-2">
<div class="flex justify-between text-sm text-gray-400 mb-1">
<span data-i18n="settings.progress">Selection Progress</span>
<span id="selection-percentage">0%</span>
</div>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div id="selection-progress-bar" class="progress-bar h-2.5 rounded-full"></div>
</div>
</div>
</div>
<!-- Boss Settings -->
<div>
<h3 class="text-2xl font-bold mb-6 text-white flex items-center">
<i class="fas fa-dragon mr-3 text-red-400"></i>
<span data-i18n="settings.bossSettings">Boss & Legend Settings</span>
</h3>
<p class="text-gray-400 mb-6" data-i18n="settings.bossSettingsDesc">
Toggle which bosses and legends should be included in randomization
</p>
<!-- Category Selector -->
<div class="mb-6">
<div class="card-gradient p-6 rounded-2xl">
<label for="boss-category-select" class="block text-sm font-semibold mb-3 text-gray-300 flex items-center">
<i class="fas fa-filter mr-2 text-blue-400"></i>
<span data-i18n="settings.category">Category:</span>
</label>
<div class="flex flex-wrap gap-3">
<button data-boss-category="world-bosses" class="boss-category-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105 flex items-center">
<i class="fas fa-dragon mr-2 text-blue-400"></i>
<span data-i18n="settings.categories.worldBosses">World Bosses</span>
<span class="ml-2 px-2 py-1 text-xs bg-blue-900 text-blue-200 rounded-full" id="world-boss-count">40</span>
</button>
<button data-boss-category="weekly-bosses" class="boss-category-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105 flex items-center">
<i class="fas fa-crown mr-2 text-yellow-400"></i>
<span data-i18n="settings.categories.weeklyBosses">Weekly Bosses</span>
<span class="ml-2 px-2 py-1 text-xs bg-yellow-900 text-yellow-200 rounded-full" id="weekly-boss-count">12</span>
</button>
<button data-boss-category="local-legends" class="boss-category-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105 flex items-center active">
<i class="fas fa-landmark mr-2 text-pink-400"></i>
<span data-i18n="settings.categories.localLegends">Local Legends</span>
<span class="ml-2 px-2 py-1 text-xs bg-pink-900 text-pink-200 rounded-full" id="legend-count">30</span>
</button>
</div>
</div>
</div>
<!-- Boss Toggles -->
<div id="bosses-settings-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[500px] overflow-y-auto p-2 custom-scrollbar">
<!-- Boss toggles will be dynamically added here -->
</div>
<!-- Bulk Actions -->
<div class="flex flex-wrap gap-3 mt-6">
<button id="enable-all-bosses" class="px-4 py-2 rounded-xl gradient-success text-white font-semibold hover:scale-105 transition-transform">
<i class="fas fa-check-circle mr-2"></i>
<span data-i18n="settings.enableAll">Enable All</span>
</button>
<button id="disable-all-bosses" class="px-4 py-2 rounded-xl gradient-danger text-white font-semibold hover:scale-105 transition-transform">
<i class="fas fa-times-circle mr-2"></i>
<span data-i18n="settings.disableAll">Disable All</span>
</button>
<button id="reset-bosses" class="px-4 py-2 rounded-xl glass font-semibold hover:scale-105 transition-transform">
<i class="fas fa-undo mr-2"></i>
<span data-i18n="settings.reset">Reset to Default</span>
</button>
</div>
</div>
</div>
<!-- Characters Section -->
<section id="characters-section" class="mb-20 glass rounded-3xl p-8 relative overflow-hidden section-padding">
<!-- Background Decoration -->
<div class="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-purple-500/10 to-pink-500/10 rounded-full -translate-y-32 translate-x-32"></div>
<div class="absolute bottom-0 left-0 w-96 h-96 bg-gradient-to-tr from-blue-500/10 to-cyan-500/10 rounded-full translate-y-48 -translate-x-48"></div>
<div class="relative z-10">
<div class="text-center mb-10">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full gradient-primary mb-6">
<i class="fas fa-users text-white text-4xl"></i>
</div>
<h2 class="text-5xl font-bold mb-4 text-white" data-i18n="sections.characters.title">
Character Team Roulette
</h2>
<p class="text-gray-300 text-lg max-w-2xl mx-auto" data-i18n="sections.characters.subtitle">
Spin to get a random team of characters for your next adventure
</p>
</div>
<!-- Team Configuration -->
<div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-6">
<div class="flex-1 w-full">
<div class="card-gradient p-6 rounded-2xl card-padding">
<div class="flex flex-col md:flex-row justify-between items-center gap-4">
<div>
<h4 class="font-bold text-lg text-white mb-2" data-i18n="sections.characters.config">Team Configuration</h4>
<p class="text-gray-400 text-sm" data-i18n="sections.characters.configDesc">
Configure your team before spinning
</p>
</div>
<div class="flex items-center space-x-6">
<div class="text-center">
<div class="text-3xl font-bold text-white" id="current-team-size">4</div>
<div class="text-xs text-gray-400" data-i18n="sections.characters.teamSize">Team Size</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-white" id="available-chars">120</div>
<div class="text-xs text-gray-400" data-i18n="sections.characters.available">Available</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-white" id="owned-chars">0</div>
<div class="text-xs text-gray-400" data-i18n="sections.characters.owned">Owned</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex-shrink-0 mt-4 md:mt-0">
<button id="spin-characters-button"
class="px-14 py-6 rounded-2xl gradient-primary text-white font-bold text-xl hover:scale-105 transition-transform duration-300 shadow-lg hover:shadow-xl relative overflow-hidden group pulse button-padding">
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
<div class="relative z-10 flex items-center justify-center">
<i class="fas fa-dice-d20 mr-3 text-2xl"></i>
<span data-i18n="buttons.spinTeam">Spin Team!</span>
</div>
</button>
</div>
</div>
<!-- Result Area -->
<div id="characters-result" class="result-container rounded-2xl p-8 mb-8">
<div class="text-center max-w-md mx-auto">
<div class="text-7xl mb-6 float-animation">🎮</div>
<p id="characters-placeholder" class="text-gray-400 text-xl mb-4" data-i18n="placeholders.characters">
Your random team will appear here
</p>
<p class="text-gray-500 text-sm" data-i18n="placeholders.charactersHint">
Click the "Spin Team!" button to generate a random team
</p>
</div>
<div id="characters-loader" class="loader hidden"></div>
</div>
<!-- Save Team Section -->
<div id="save-characters-section" class="mt-8 flex flex-col md:flex-row justify-center items-center gap-4 hidden">
<div class="relative flex-1 w-full md:w-auto">
<i class="fas fa-tag absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
<input type="text" id="team-name-input"
placeholder="Give your team a name (optional)"
data-i18n-placeholder="placeholders.teamName"
class="w-full pl-12 pr-4 py-4 rounded-2xl input-field focus:ring-2 focus:ring-purple-500">
</div>
<button id="save-team-button"
class="px-8 py-4 rounded-2xl gradient-secondary text-white font-bold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-save mr-3"></i>
<span data-i18n="buttons.saveTeam">Save Team</span>
</button>
<button id="share-team-button"
class="px-8 py-4 rounded-2xl glass font-bold hover:scale-105 transition-transform flex items-center hidden">
<i class="fas fa-share-alt mr-3"></i>
<span data-i18n="buttons.share">Share</span>
</button>
</div>
</div>
</section>
<!-- World Bosses Section -->
<section id="world-bosses-section" class="mb-20 glass rounded-3xl p-8 hidden relative overflow-hidden section-padding">
<!-- Background Decoration -->
<div class="absolute top-0 left-0 w-64 h-64 bg-gradient-to-br from-blue-500/10 to-cyan-500/10 rounded-full -translate-y-32 -translate-x-32"></div>
<div class="absolute bottom-0 right-0 w-96 h-96 bg-gradient-to-tr from-purple-500/10 to-pink-500/10 rounded-full translate-y-48 translate-x-48"></div>
<div class="relative z-10">
<div class="text-center mb-10">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full gradient-accent mb-6">
<i class="fas fa-dragon text-white text-4xl"></i>
</div>
<h2 class="text-5xl font-bold mb-4 text-white" data-i18n="sections.worldBosses.title">
World Boss Roulette
</h2>
<p class="text-gray-300 text-lg max-w-2xl mx-auto" data-i18n="sections.worldBosses.subtitle">
Challenge a random world boss and test your skills
</p>
</div>
<div class="flex justify-center mb-10">
<button id="spin-world-boss-button"
class="px-14 py-6 rounded-2xl gradient-accent text-white font-bold text-xl hover:scale-105 transition-transform duration-300 shadow-lg hover:shadow-xl relative overflow-hidden group button-padding">
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
<div class="relative z-10 flex items-center justify-center">
<i class="fas fa-dragon mr-3 text-2xl"></i>
<span data-i18n="buttons.spinBoss">Spin Boss!</span>
</div>
</button>
</div>
<div id="world-boss-result" class="result-container rounded-2xl p-8 mb-8">
<div class="text-center max-w-md mx-auto">
<div class="text-7xl mb-6 float-animation">🐉</div>
<p id="world-boss-placeholder" class="text-gray-400 text-xl mb-4" data-i18n="placeholders.worldBoss">
Random world boss will appear here
</p>
<p class="text-gray-500 text-sm" data-i18n="placeholders.worldBossHint">
Click the "Spin Boss!" button to generate a random world boss
</p>
</div>
<div id="world-boss-loader" class="loader hidden"></div>
</div>
<div id="save-world-boss-section" class="mt-8 flex justify-center hidden">
<button id="save-world-boss-button"
class="px-8 py-4 rounded-2xl gradient-secondary text-white font-bold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-save mr-3"></i>
<span data-i18n="buttons.saveToHistory">Save to History</span>
</button>
</div>
</div>
</section>
<!-- Weekly Bosses Section -->
<section id="weekly-bosses-section" class="mb-20 glass rounded-3xl p-8 hidden relative overflow-hidden section-padding">
<!-- Background Decoration -->
<div class="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-yellow-500/10 to-orange-500/10 rounded-full -translate-y-32 translate-x-32"></div>
<div class="absolute bottom-0 left-0 w-96 h-96 bg-gradient-to-tr from-red-500/10 to-pink-500/10 rounded-full translate-y-48 -translate-x-48"></div>
<div class="relative z-10">
<div class="text-center mb-10">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full gradient-gold mb-6">
<i class="fas fa-crown text-white text-4xl"></i>
</div>
<h2 class="text-5xl font-bold mb-4 text-white" data-i18n="sections.weeklyBosses.title">
Weekly Boss Roulette
</h2>
<p class="text-gray-300 text-lg max-w-2xl mx-auto" data-i18n="sections.weeklyBosses.subtitle">
Face a random weekly boss for valuable rewards
</p>
</div>
<div class="flex justify-center mb-10">
<button id="spin-weekly-boss-button"
class="px-14 py-6 rounded-2xl gradient-gold text-white font-bold text-xl hover:scale-105 transition-transform duration-300 shadow-lg hover:shadow-xl relative overflow-hidden group button-padding">
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
<div class="relative z-10 flex items-center justify-center">
<i class="fas fa-crown mr-3 text-2xl"></i>
<span data-i18n="buttons.spinWeeklyBoss">Spin Weekly Boss!</span>
</div>
</button>
</div>
<div id="weekly-boss-result" class="result-container rounded-2xl p-8 mb-8">
<div class="text-center max-w-md mx-auto">
<div class="text-7xl mb-6 float-animation">⚔️</div>
<p id="weekly-boss-placeholder" class="text-gray-400 text-xl mb-4" data-i18n="placeholders.weeklyBoss">
Random weekly boss will appear here
</p>
<p class="text-gray-500 text-sm" data-i18n="placeholders.weeklyBossHint">
Click the "Spin Weekly Boss!" button to generate a random weekly boss
</p>
</div>
<div id="weekly-boss-loader" class="loader hidden"></div>
</div>
<div id="save-weekly-boss-section" class="mt-8 flex justify-center hidden">
<button id="save-weekly-boss-button"
class="px-8 py-4 rounded-2xl gradient-secondary text-white font-bold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-save mr-3"></i>
<span data-i18n="buttons.saveToHistory">Save to History</span>
</button>
</div>
</div>
</section>
<!-- Local Legends Section -->
<section id="local-legends-section" class="mb-20 glass rounded-3xl p-8 hidden relative overflow-hidden section-padding">
<!-- Background Decoration -->
<div class="absolute top-0 left-0 w-64 h-64 bg-gradient-to-br from-pink-500/10 to-purple-500/10 rounded-full -translate-y-32 -translate-x-32"></div>
<div class="absolute bottom-0 right-0 w-96 h-96 bg-gradient-to-tr from-green-500/10 to-cyan-500/10 rounded-full translate-y-48 translate-x-48"></div>
<div class="relative z-10">
<div class="text-center mb-10">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full gradient-purple mb-6">
<i class="fas fa-landmark text-white text-4xl"></i>
</div>
<h2 class="text-5xl font-bold mb-4 text-white" data-i18n="sections.localLegends.title">
Local Legends Roulette
</h2>
<p class="text-gray-300 text-lg max-w-2xl mx-auto" data-i18n="sections.localLegends.subtitle">
Discover and challenge random local legends across Teyvat
</p>
</div>
<div class="flex justify-center mb-10">
<button id="spin-legend-button"
class="px-14 py-6 rounded-2xl gradient-purple text-white font-bold text-xl hover:scale-105 transition-transform duration-300 shadow-lg hover:shadow-xl relative overflow-hidden group button-padding">
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000"></div>
<div class="relative z-10 flex items-center justify-center">
<i class="fas fa-landmark mr-3 text-2xl"></i>
<span data-i18n="buttons.spinLegend">Spin Legend!</span>
</div>
</button>
</div>
<div id="legend-result" class="result-container rounded-2xl p-8 mb-8">
<div class="text-center max-w-md mx-auto">
<div class="text-7xl mb-6 float-animation">🏆</div>
<p id="legend-placeholder" class="text-gray-400 text-xl mb-4" data-i18n="placeholders.legend">
Random local legend will appear here
</p>
<p class="text-gray-500 text-sm" data-i18n="placeholders.legendHint">
Click the "Spin Legend!" button to discover a random local legend
</p>
</div>
<div id="legend-loader" class="loader hidden"></div>
</div>
<div id="save-legend-section" class="mt-8 flex justify-center hidden">
<button id="save-legend-button"
class="px-8 py-4 rounded-2xl gradient-secondary text-white font-bold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-save mr-3"></i>
<span data-i18n="buttons.saveToHistory">Save to History</span>
</button>
</div>
</div>
</section>
<!-- History Section -->
<section id="history-section" class="glass rounded-3xl p-8 relative overflow-hidden section-padding">
<!-- Background Decoration -->
<div class="absolute top-0 right-0 w-48 h-48 bg-gradient-to-br from-gray-500/10 to-gray-700/10 rounded-full -translate-y-24 translate-x-24"></div>
<div class="relative z-10">
<div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-6">
<div>
<h2 class="text-5xl font-bold text-white flex items-center">
<i class="fas fa-history mr-4 text-purple-400"></i>
<span data-i18n="history.title">History</span>
</h2>
<p class="text-gray-400 mt-2" data-i18n="history.subtitle">
Your recent random selections
</p>
</div>
<div class="flex flex-wrap gap-3">
<button id="show-full-history-button"
class="px-5 py-3 rounded-2xl glass font-semibold hover:scale-105 transition-transform flex items-center hidden">
<i class="fas fa-list-ol mr-3"></i>
<span data-i18n="buttons.fullHistory">Full History</span>
</button>
<button id="clear-history-button"
class="px-5 py-3 rounded-2xl gradient-danger text-white font-semibold hover:scale-105 transition-transform flex items-center hidden">
<i class="fas fa-trash-alt mr-3"></i>
<span data-i18n="buttons.clearHistory">Clear History</span>
</button>
<button id="export-history-button"
class="px-5 py-3 rounded-2xl glass font-semibold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-download mr-3"></i>
<span data-i18n="buttons.export">Export</span>
</button>
</div>
</div>
<!-- History Filters -->
<div class="mb-8">
<div class="flex flex-wrap gap-3">
<button data-history-filter="all" class="history-filter-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105 active">
<i class="fas fa-layer-group mr-2"></i>
<span data-i18n="history.filters.all">All</span>
</button>
<button data-history-filter="team" class="history-filter-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105">
<i class="fas fa-users mr-2 text-purple-400"></i>
<span data-i18n="history.filters.teams">Teams</span>
</button>
<button data-history-filter="world-boss" class="history-filter-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105">
<i class="fas fa-dragon mr-2 text-blue-400"></i>
<span data-i18n="history.filters.worldBosses">World Bosses</span>
</button>
<button data-history-filter="weekly-boss" class="history-filter-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105">
<i class="fas fa-crown mr-2 text-yellow-400"></i>
<span data-i18n="history.filters.weeklyBosses">Weekly Bosses</span>
</button>
<button data-history-filter="legend" class="history-filter-btn px-4 py-2 rounded-xl glass font-medium transition-all hover:scale-105">
<i class="fas fa-landmark mr-2 text-pink-400"></i>
<span data-i18n="history.filters.legends">Legends</span>
</button>
</div>
</div>
<!-- History Content -->
<div id="history-content" class="space-y-6">
<p id="history-placeholder" class="text-center text-gray-400 text-lg py-12" data-i18n="placeholders.history">
Your random selections history will appear here
</p>
</div>
<!-- History Stats -->
<div class="mt-10 pt-8 border-t border-gray-700">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-center p-4 rounded-2xl glass stat-card">
<div class="text-2xl font-bold text-white" id="history-total">0</div>
<div class="text-sm text-gray-400" data-i18n="history.stats.total">Total Entries</div>
</div>
<div class="text-center p-4 rounded-2xl glass stat-card">
<div class="text-2xl font-bold text-white" id="history-teams">0</div>
<div class="text-sm text-gray-400" data-i18n="history.stats.teams">Teams</div>
</div>
<div class="text-center p-4 rounded-2xl glass stat-card">
<div class="text-2xl font-bold text-white" id="history-bosses">0</div>
<div class="text-sm text-gray-400" data-i18n="history.stats.bosses">Bosses</div>
</div>
<div class="text-center p-4 rounded-2xl glass stat-card">
<div class="text-2xl font-bold text-white" id="history-legends">0</div>
<div class="text-sm text-gray-400" data-i18n="history.stats.legends">Legends</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="text-center mt-20 pt-10 border-t border-gray-700">
<div class="flex flex-col lg:flex-row justify-between items-center mb-8 gap-8">
<div class="text-left">
<div class="flex items-center mb-4">
<div class="w-12 h-12 rounded-full gradient-primary flex items-center justify-center mr-3">
<i class="fas fa-dice text-white"></i>
</div>
<h3 class="text-xl font-bold text-white">Genshin Roulette</h3>
</div>
<p class="text-sm text-gray-400 max-w-md" data-i18n="footer.disclaimer">
This is a fan-made project. Not affiliated with or endorsed by HoYoverse.
</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-6">
</div>
</div>
<div class="flex flex-col md:flex-row justify-between items-center pt-8 border-t border-gray-700 gap-4">
<div class="flex items-center space-x-6">
<span class="text-sm text-gray-400">
<span data-i18n="footer.version">Version:</span> <span class="font-mono">2.1.0</span>
</span>
<span class="text-sm text-gray-400">
<span data-i18n="footer.characters">Characters:</span> <span class="font-bold">120</span>
</span>
<span class="text-sm text-gray-400">
<span data-i18n="footer.bosses">Bosses:</span> <span class="font-bold">82</span>
</span>
</div>
<div class="flex items-center space-x-4">
<button id="back-to-top" class="p-2 rounded-full glass hover:scale-105 transition-transform tooltip" data-tooltip="Back to top">
<i class="fas fa-arrow-up text-gray-300"></i>
</button>
<a href="#" class="p-2 rounded-full glass hover:scale-105 transition-transform tooltip" data-tooltip="GitHub">
<i class="fab fa-github text-gray-300"></i>
</a>
<a href="#" class="p-2 rounded-full glass hover:scale-105 transition-transform tooltip" data-tooltip="Discord">
<i class="fab fa-discord text-gray-300"></i>
</a>
</div>
</div>
</footer>
</div>
<!-- Modals -->
<div id="confirm-modal" class="fixed inset-0 bg-black/70 z-50 flex justify-center items-center hidden p-4 modal-backdrop">
<div class="glass rounded-3xl w-full max-w-md shadow-2xl p-8 text-center modal-content">
<div class="w-20 h-20 rounded-full gradient-danger flex items-center justify-center mx-auto mb-6">
<i class="fas fa-exclamation text-white text-3xl"></i>
</div>
<h3 class="text-2xl font-bold mb-4 text-white" data-i18n="modals.confirm.title">Confirmation</h3>
<p id="confirm-modal-message" class="text-gray-300 mb-8 text-lg" data-i18n="modals.confirm.message">
Are you sure you want to clear history?
</p>
<div class="flex justify-center gap-4">
<button id="confirm-modal-cancel" class="px-8 py-3 rounded-2xl glass font-semibold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-times mr-3"></i>
<span data-i18n="buttons.cancel">Cancel</span>
</button>
<button id="confirm-modal-ok" class="px-8 py-3 rounded-2xl gradient-danger text-white font-semibold hover:scale-105 transition-transform flex items-center">
<i class="fas fa-check mr-3"></i>
<span data-i18n="buttons.confirm">Confirm</span>
</button>
</div>
</div>
</div>
<div id="full-history-modal" class="fixed inset-0 bg-black/70 z-50 flex justify-center items-center hidden p-4 modal-backdrop">
<div class="glass rounded-3xl w-full max-w-6xl h-[90vh] flex flex-col p-8 modal-content">
<div class="flex justify-between items-center mb-8">
<h2 class="text-4xl font-bold text-white flex items-center">
<i class="fas fa-history mr-4 text-purple-400"></i>
<span data-i18n="modals.fullHistory.title">Full History</span>
</h2>
<button id="full-history-modal-close" class="p-3 rounded-2xl glass hover:scale-105 transition-transform">
<i class="fas fa-times text-gray-300 text-xl"></i>
</button>
</div>
<div id="full-history-modal-content-inner" class="flex-1 overflow-y-auto pr-4 space-y-6 custom-scrollbar">
<!-- History items will be dynamically added here -->
</div>
<div class="text-center mt-8 pt-6 border-t border-gray-700">
<button id="full-history-modal-close-bottom" class="px-8 py-3 rounded-2xl glass font-semibold hover:scale-105 transition-transform flex items-center mx-auto">
<i class="fas fa-times mr-3"></i>
<span data-i18n="buttons.close">Close</span>
</button>
</div>
</div>
</div>
<!-- Help Modal -->
<div id="help-modal" class="fixed inset-0 bg-black/70 z-50 flex justify-center items-center hidden p-4 modal-backdrop">
<div class="glass rounded-3xl w-full max-w-4xl h-[80vh] flex flex-col p-8 modal-content">
<div class="flex justify-between items-center mb-8">
<h2 class="text-4xl font-bold text-white flex items-center">
<i class="fas fa-question-circle mr-4 text-yellow-400"></i>
<span data-i18n="modals.help.title">Help & Guide</span>
</h2>
<button id="help-modal-close" class="p-3 rounded-2xl glass hover:scale-105 transition-transform">
<i class="fas fa-times text-gray-300 text-xl"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto pr-4 space-y-8 custom-scrollbar">
<div>
<h3 class="text-2xl font-bold text-white mb-4" data-i18n="help.gettingStarted">Getting Started</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="card-gradient p-6 rounded-2xl">
<div class="w-12 h-12 rounded-full gradient-primary flex items-center justify-center mb-4">
<i class="fas fa-users text-white"></i>
</div>
<h4 class="font-bold text-lg text-white mb-2" data-i18n="help.selectCharacters">Select Your Characters</h4>
<p class="text-gray-400" data-i18n="help.selectCharactersDesc">
Go to Settings → My Characters and select all characters you own. This ensures the roulette only uses characters you have.
</p>
</div>
<div class="card-gradient p-6 rounded-2xl">
<div class="w-12 h-12 rounded-full gradient-accent flex items-center justify-center mb-4">
<i class="fas fa-sliders-h text-white"></i>
</div>
<h4 class="font-bold text-lg text-white mb-2" data-i18n="help.configureSettings">Configure Settings</h4>
<p class="text-gray-400" data-i18n="help.configureSettingsDesc">
Adjust team size, animation speed, and boss preferences in the settings panel to customize your experience.
</p>
</div>
</div>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4" data-i18n="help.features">Features</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="card-gradient p-6 rounded-2xl">
<h4 class="font-bold text-lg text-white mb-2 flex items-center">
<i class="fas fa-users mr-2 text-purple-400"></i>
<span data-i18n="help.teamRoulette">Team Roulette</span>
</h4>
<p class="text-gray-400 text-sm" data-i18n="help.teamRouletteDesc">
Generate random teams of 1-4 characters for new gameplay challenges.
</p>
</div>
<div class="card-gradient p-6 rounded-2xl">
<h4 class="font-bold text-lg text-white mb-2 flex items-center">
<i class="fas fa-dragon mr-2 text-blue-400"></i>
<span data-i18n="help.bossRoulette">Boss Roulette</span>
</h4>
<p class="text-gray-400 text-sm" data-i18n="help.bossRouletteDesc">
Get random world bosses, weekly bosses, or local legends to challenge.
</p>
</div>
<div class="card-gradient p-6 rounded-2xl">
<h4 class="font-bold text-lg text-white mb-2 flex items-center">
<i class="fas fa-history mr-2 text-green-400"></i>
<span data-i18n="help.history">History & Stats</span>
</h4>
<p class="text-gray-400 text-sm" data-i18n="help.historyDesc">
Track all your generated teams and bosses with detailed statistics.
</p>
</div>
</div>
</div>
<div>
<h3 class="text-2xl font-bold text-white mb-4" data-i18n="help.tips">Tips & Tricks</h3>
<div class="space-y-4">
<div class="flex items-start">
<div class="w-8 h-8 rounded-full bg-green-900 flex items-center justify-center mr-4 flex-shrink-0">
<i class="fas fa-lightbulb text-green-400"></i>
</div>
<p class="text-gray-400" data-i18n="help.tip1">
Use the "My Characters" feature to limit the roulette to only characters you own for more realistic teams.
</p>
</div>
<div class="flex items-start">
<div class="w-8 h-8 rounded-full bg-blue-900 flex items-center justify-center mr-4 flex-shrink-0">
<i class="fas fa-lightbulb text-blue-400"></i>
</div>
<p class="text-gray-400" data-i18n="help.tip2">
Save your favorite teams with custom names for quick reference later.
</p>
</div>
<div class="flex items-start">
<div class="w-8 h-8 rounded-full bg-purple-900 flex items-center justify-center mr-4 flex-shrink-0">
<i class="fas fa-lightbulb text-purple-400"></i>
</div>
<p class="text-gray-400" data-i18n="help.tip3">
Enable sound effects for a more immersive spinning experience.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Audio Elements -->
<audio id="spin-sound" src="STREAMING-casino-game-wheel-spin-bosnow-1-00-07.mp3" preload="auto"></audio>
<audio id="reveal-sound" src="STREAMING-cinematic-transition-boom-piano-hit-tomas-herudek-4-4-00-08.mp3" preload="auto"></audio>
<audio id="click-sound" src="STREAMING-futuristic-ui-positive-selection-davies-aguirre-2-2-00-00.mp3" preload="auto"></audio>
<audio id="success-sound" src="STREAMING-ui-high-button-beep-epic-stock-media-3-3-00-00.mp3" preload="auto"></audio>
<!-- JavaScript Implementation -->
<script>
// ==================== TRANSLATIONS ====================
const translations = {
en: {
// UI Elements
"title": "Genshin Roulette",
"subtitle": "Random characters, bosses & legends for your Genshin Impact adventure",
"footer.disclaimer": "This is a fan-made project. Not affiliated with or endorsed by HoYoverse.",
"footer.version": "Version:",
"footer.characters": "Characters:",
"footer.bosses": "Bosses:",
// Stats
"stats.characters": "Characters",
"stats.worldBosses": "World Bosses",
"stats.weeklyBosses": "Weekly Bosses",
"stats.legends": "Legends",
"stats.owned": "Owned:",
"stats.available": "Available:",
// Tabs
"tab.characters": "Character Team",
"tab.worldBosses": "World Bosses",
"tab.weeklyBosses": "Weekly Bosses",
"tab.localLegends": "Local Legends",
// Sections Titles
"sections.characters.title": "Character Team Roulette",
"sections.characters.subtitle": "Spin to get a random team of characters for your next adventure",
"sections.characters.config": "Team Configuration",
"sections.characters.configDesc": "Configure your team before spinning",
"sections.characters.teamSize": "Team Size",
"sections.characters.available": "Available",
"sections.characters.owned": "Owned",
"sections.worldBosses.title": "World Boss Roulette",
"sections.worldBosses.subtitle": "Challenge a random world boss and test your skills",
"sections.weeklyBosses.title": "Weekly Boss Roulette",
"sections.weeklyBosses.subtitle": "Face a random weekly boss for valuable rewards",
"sections.localLegends.title": "Local Legends Roulette",
"sections.localLegends.subtitle": "Discover and challenge random local legends across Teyvat",
// Buttons
"buttons.spinTeam": "Spin Team!",
"buttons.spinBoss": "Spin Boss!",
"buttons.spinWeeklyBoss": "Spin Weekly Boss!",
"buttons.spinLegend": "Spin Legend!",
"buttons.saveTeam": "Save Team",
"buttons.saveToHistory": "Save to History",
"buttons.share": "Share",
"buttons.fullHistory": "Full History",
"buttons.clearHistory": "Clear History",
"buttons.export": "Export",
"buttons.cancel": "Cancel",
"buttons.confirm": "Confirm",
"buttons.close": "Close",
"buttons.viewGuide": "View Guide",
// Settings
"settings.title": "Roulette Settings",
"settings.sound": "Sound:",
"settings.animations": "Animations:",
"settings.teamSize": "Team Size",
"settings.animationSpeed": "Animation Speed",
"settings.duplicates": "Allow Duplicates",
"settings.duplicates.no": "No Duplicates",
"settings.duplicates.yes": "Allow Duplicates",
"settings.myCharacters": "My Characters",
"settings.myCharactersDesc": "Select characters you own. If none selected, all characters will be used.",
"settings.searchPlaceholder": "Search characters by name...",
"settings.selected": "Selected:",
"settings.selectAll": "Select All",
"settings.clearSelected": "Clear All",
"settings.progress": "Selection Progress",
"settings.bossSettings": "Boss & Legend Settings",
"settings.bossSettingsDesc": "Toggle which bosses and legends should be included in randomization",
"settings.category": "Category:",
"settings.categories.worldBosses": "World Bosses",
"settings.categories.weeklyBosses": "Weekly Bosses",
"settings.categories.localLegends": "Local Legends",
"settings.enableAll": "Enable All",
"settings.disableAll": "Disable All",
"settings.reset": "Reset to Default",
"settings.speed.fast": "Fast",
"settings.speed.medium": "Medium",
"settings.speed.slow": "Slow",
// Filters
"filters.allElements": "All Elements",
// Placeholders
"placeholders.characters": "Your random team will appear here",
"placeholders.charactersHint": "Click the 'Spin Team!' button to generate a random team",
"placeholders.teamName": "Give your team a name (optional)",
"placeholders.worldBoss": "Random world boss will appear here",
"placeholders.worldBossHint": "Click the 'Spin Boss!' button to generate a random world boss",
"placeholders.weeklyBoss": "Random weekly boss will appear here",
"placeholders.weeklyBossHint": "Click the 'Spin Weekly Boss!' button to generate a random weekly boss",
"placeholders.legend": "Random local legend will appear here",
"placeholders.legendHint": "Click the 'Spin Legend!' button to discover a random local legend",
"placeholders.history": "Your random selections history will appear here",
// History
"history.title": "History",
"history.subtitle": "Your recent random selections",
"history.filters.all": "All",
"history.filters.teams": "Teams",
"history.filters.worldBosses": "World Bosses",
"history.filters.weeklyBosses": "Weekly Bosses",
"history.filters.legends": "Legends",
"history.stats.total": "Total Entries",
"history.stats.teams": "Teams",
"history.stats.bosses": "Bosses",
"history.stats.legends": "Legends",
// Modals
"modals.confirm.title": "Confirmation",
"modals.confirm.message": "Are you sure you want to clear history?",
"modals.fullHistory.title": "Full History",
"modals.help.title": "Help & Guide",
// Help
"help.gettingStarted": "Getting Started",
"help.selectCharacters": "Select Your Characters",
"help.selectCharactersDesc": "Go to Settings → My Characters and select all characters you own. This ensures the roulette only uses characters you have.",
"help.configureSettings": "Configure Settings",
"help.configureSettingsDesc": "Adjust team size, animation speed, and boss preferences in the settings panel to customize your experience.",
"help.features": "Features",
"help.teamRoulette": "Team Roulette",
"help.teamRouletteDesc": "Generate random teams of 1-4 characters for new gameplay challenges.",
"help.bossRoulette": "Boss Roulette",
"help.bossRouletteDesc": "Get random world bosses, weekly bosses, or local legends to challenge.",
"help.history": "History & Stats",
"help.historyDesc": "Track all your generated teams and bosses with detailed statistics.",
"help.tips": "Tips & Tricks",
"help.tip1": "Use the 'My Characters' feature to limit the roulette to only characters you own for more realistic teams.",
"help.tip2": "Save your favorite teams with custom names for quick reference later.",
"help.tip3": "Enable sound effects for a more immersive spinning experience.",
// Element Names
"elements.pyro": "Pyro",
"elements.hydro": "Hydro",
"elements.anemo": "Anemo",
"elements.electro": "Electro",
"elements.dendro": "Dendro",
"elements.cryo": "Cryo",
"elements.geo": "Geo"
},
ru: {
// UI Elements
"title": "Рулетка Genshin",
"subtitle": "Случайные персонажи, боссы и легенды для вашего приключения в Genshin Impact",
"footer.disclaimer": "Это фанатский проект. Не аффилирован и не одобрен HoYoverse.",
"footer.version": "Версия:",
"footer.characters": "Персонажи:",
"footer.bosses": "Боссы:",
// Stats
"stats.characters": "Персонажи",
"stats.worldBosses": "Мировые боссы",
"stats.weeklyBosses": "Еженедельные боссы",
"stats.legends": "Легенды",
"stats.owned": "Ваши:",
"stats.available": "Доступно:",
// Tabs
"tab.characters": "Отряд персонажей",
"tab.worldBosses": "Мировые боссы",
"tab.weeklyBosses": "Еженедельные боссы",
"tab.localLegends": "Местные легенды",
// Sections Titles
"sections.characters.title": "Рандом отряда персонажей",
"sections.characters.subtitle": "Случайный подбор команды персонажей для вашего следующего приключения",
"sections.characters.config": "Настройка отряда",
"sections.characters.configDesc": "Настройте отряд перед вращением",
"sections.characters.teamSize": "Размер отряда",
"sections.characters.available": "Доступно",
"sections.characters.owned": "Ваши",
"sections.worldBosses.title": "Рандом мирового босса",
"sections.worldBosses.subtitle": "Сразитесь со случайным мировым боссом и проверьте свои навыки",
"sections.weeklyBosses.title": "Рандом еженедельного босса",
"sections.weeklyBosses.subtitle": "Сразитесь со случайным еженедельным боссом за ценные награды",
"sections.localLegends.title": "Рандом местной легенды",
"sections.localLegends.subtitle": "Откройте и сразитесь со случайными местными легендами по всему Тейвату",
// Buttons
"buttons.spinTeam": "Крутить отряд!",
"buttons.spinBoss": "Выбрать босса!",
"buttons.spinWeeklyBoss": "Выбрать босса!",
"buttons.spinLegend": "Выбрать легенду!",
"buttons.saveTeam": "Сохранить отряд",
"buttons.saveToHistory": "Сохранить в историю",
"buttons.share": "Поделиться",
"buttons.fullHistory": "Вся история",
"buttons.clearHistory": "Очистить историю",
"buttons.export": "Экспорт",
"buttons.cancel": "Отмена",
"buttons.confirm": "Подтвердить",
"buttons.close": "Закрыть",
"buttons.viewGuide": "Открыть гайд",
// Settings
"settings.title": "Настройки рулетки",
"settings.sound": "Звук:",
"settings.animations": "Анимации:",
"settings.teamSize": "Количество в отряде",
"settings.animationSpeed": "Скорость анимации",
"settings.duplicates": "Повторения",
"settings.duplicates.no": "Без повторений",
"settings.duplicates.yes": "Разрешить повторения",
"settings.myCharacters": "Мои персонажи",
"settings.myCharactersDesc": "Выберите персонажей, которыми вы владеете. Если ни один не выбран, будут использоваться все персонажи.",
"settings.searchPlaceholder": "Поиск персонажей по имени...",
"settings.selected": "Выбрано:",
"settings.selectAll": "Выбрать всех",
"settings.clearSelected": "Очистить всё",
"settings.progress": "Прогресс выбора",
"settings.bossSettings": "Настройки боссов и легенд",
"settings.bossSettingsDesc": "Выберите, какие боссы и легенды должны участвовать в рандомизации",
"settings.category": "Категория:",
"settings.categories.worldBosses": "Мировые боссы",
"settings.categories.weeklyBosses": "Еженедельные боссы",
"settings.categories.localLegends": "Местные легенды",
"settings.enableAll": "Включить все",
"settings.disableAll": "Отключить все",
"settings.reset": "Сбросить",
"settings.speed.fast": "Быстрая",
"settings.speed.medium": "Средняя",
"settings.speed.slow": "Медленная",
// Filters
"filters.allElements": "Все элементы",
// Placeholders
"placeholders.characters": "Ваш случайный отряд появится здесь",
"placeholders.charactersHint": "Нажмите кнопку 'Крутить отряд!' для создания случайного отряда",
"placeholders.teamName": "Дайте название отряду (необязательно)",
"placeholders.worldBoss": "Случайный мировой босс появится здесь",
"placeholders.worldBossHint": "Нажмите кнопку 'Выбрать босса!' для выбора случайного мирового босса",
"placeholders.weeklyBoss": "Случайный еженедельный босс появится здесь",
"placeholders.weeklyBossHint": "Нажмите кнопку 'Выбрать босса!' для выбора случайного еженедельного босса",
"placeholders.legend": "Случайная местная легенда появится здесь",
"placeholders.legendHint": "Нажмите кнопку 'Выбрать легенду!' для выбора случайной местной легенды",
"placeholders.history": "История ваших случайных выборов будет отображаться здесь",
// History
"history.title": "История",
"history.subtitle": "Ваши последние случайные выборы",
"history.filters.all": "Все",
"history.filters.teams": "Отряды",
"history.filters.worldBosses": "Мировые боссы",
"history.filters.weeklyBosses": "Еженедельные боссы",
"history.filters.legends": "Легенды",
"history.stats.total": "Всего записей",
"history.stats.teams": "Отряды",
"history.stats.bosses": "Боссы",
"history.stats.legends": "Легенды",
// Modals
"modals.confirm.title": "Подтверждение",
"modals.confirm.message": "Вы уверены, что хотите очистить историю?",
"modals.fullHistory.title": "Вся история",
"modals.help.title": "Помощь и руководство",
// Help
"help.gettingStarted": "Начало работы",
"help.selectCharacters": "Выберите ваших персонажей",
"help.selectCharactersDesc": "Перейдите в Настройки → Мои персонажи и выберите всех персонажей, которыми вы владеете. Это гарантирует, что рулетка будет использовать только ваших персонажей.",
"help.configureSettings": "Настройте параметры",
"help.configureSettingsDesc": "Настройте размер отряда, скорость анимации и предпочтения по боссам в панели настроек для персонализации опыта.",
"help.features": "Возможности",
"help.teamRoulette": "Рандом отряда",
"help.teamRouletteDesc": "Создавайте случайные отряды из 1-4 персонажей для новых игровых испытаний.",
"help.bossRoulette": "Рандом боссов",
"help.bossRouletteDesc": "Получайте случайных мировых боссов, еженедельных боссов или местных легенд для сражений.",
"help.history": "История и статистика",
"help.historyDesc": "Отслеживайте все созданные отряды и боссов с подробной статистикой.",
"help.tips": "Советы и хитрости",
"help.tip1": "Используйте функцию 'Мои персонажи', чтобы ограничить рулетку только персонажами, которыми вы владеете, для более реалистичных отрядов.",
"help.tip2": "Сохраняйте любимые отряды с собственными названиями для быстрого доступа позже.",
"help.tip3": "Включите звуковые эффекты для более захватывающего опыта вращения.",
// Element Names
"elements.pyro": "Пиро",
"elements.hydro": "Гидро",
"elements.anemo": "Анемо",
"elements.electro": "Электро",
"elements.dendro": "Дендро",
"elements.cryo": "Крио",
"elements.geo": "Гео"
}
};
// ==================== APPLICATION INITIALIZATION ====================
document.addEventListener('DOMContentLoaded', () => {
// ==================== LANGUAGE SYSTEM ====================
let currentLanguage = localStorage.getItem('genshinLanguage') || 'en';
const languageToggle = document.getElementById('language-toggle');
const languageLabel = document.getElementById('language-label');
function setLanguage(lang) {
currentLanguage = lang;
localStorage.setItem('genshinLanguage', lang);
languageLabel.textContent = lang === 'en' ? 'RU' : 'EN';
// Update all elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
if (translations[lang][key]) {
element.textContent = translations[lang][key];
}
});
// Update placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
const key = element.getAttribute('data-i18n-placeholder');
if (translations[lang][key]) {
element.placeholder = translations[lang][key];
}
});
// Update select options
document.querySelectorAll('select option').forEach(option => {
const key = option.getAttribute('data-i18n');
if (key && translations[lang][key]) {
option.textContent = translations[lang][key];
}
});
}
languageToggle.addEventListener('click', () => {
const newLang = currentLanguage === 'en' ? 'ru' : 'en';
setLanguage(newLang);
playSound('click');
});
// Set initial language
setLanguage(currentLanguage);
// ==================== GAME DATA ====================
// Characters data from original file
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_heizou.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" },
];
// World Bosses data from original file
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-koren-yumkazavr.webp" },
{ "name": "Кукузавр-тиран золотого пламени", "icon": "pic/WorldBoss/Prozhorlivyi_-Gornyi_-koren_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/show.png" },
{ "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" }
];
// Weekly Bosses data from original file
const WEEKLY_BOSSES = [
{ "name": "Игра перед вратами", "icon": "pic/Allweeklybosses/Igra-pered-vratami.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" }
];
// Local Legends data from original file
const LOCAL_LEGENDS = [
{ "name": "Сигурд", "icon": "pic/LocalLegends/Sigurd.png", "guideUrl": "#" },
{ "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/" },
];
// Update stats
document.getElementById('total-characters').textContent = LOCAL_CHARACTERS.length;
document.getElementById('total-world-bosses').textContent = WORLD_BOSSES.length;
document.getElementById('total-weekly-bosses').textContent = WEEKLY_BOSSES.length;
document.getElementById('total-legends').textContent = LOCAL_LEGENDS.length;
document.getElementById('world-boss-count').textContent = WORLD_BOSSES.length;
document.getElementById('weekly-boss-count').textContent = WEEKLY_BOSSES.length;
document.getElementById('legend-count').textContent = LOCAL_LEGENDS.length;
document.getElementById('total-characters-count').textContent = LOCAL_CHARACTERS.length;
document.getElementById('available-chars').textContent = LOCAL_CHARACTERS.length;
// ==================== CONFIGURATION ====================
function getOrCreateUID() {
let uid = localStorage.getItem('genshinRouletteUID');
if (!uid) {
uid = 'UID-' + Math.random().toString(36).substring(2, 10).toUpperCase();
localStorage.setItem('genshinRouletteUID', uid);
}
return uid;
}
const currentUserUID = getOrCreateUID();
document.getElementById('user-uid').textContent = currentUserUID;
const config = {
teamSize: parseInt(localStorage.getItem(`${currentUserUID}_teamSize`)) || 4,
animationSpeed: localStorage.getItem(`${currentUserUID}_animationSpeed`) || 'medium',
category: localStorage.getItem('genshinCategory') || 'characters',
soundEnabled: localStorage.getItem('soundEnabled') !== 'false',
animationsEnabled: localStorage.getItem('animationsEnabled') !== 'false',
allowDuplicates: localStorage.getItem('allowDuplicates') === 'yes',
excludedBosses: JSON.parse(localStorage.getItem(`${currentUserUID}_excludedBosses`)) || {
'world-bosses': [],
'weekly-bosses': [],
'local-legends': []
}
};
// ==================== APPLICATION STATE ====================
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;
let currentGeneratedTeam = [];
let currentGeneratedBoss = null;
let isWorldBoss = true;
let isLegend = false;
// ==================== DOM ELEMENTS ====================
// Main elements
const spinCharactersButton = document.getElementById('spin-characters-button');
const spinWorldBossButton = document.getElementById('spin-world-boss-button');
const spinWeeklyBossButton = document.getElementById('spin-weekly-boss-button');
const spinLegendButton = document.getElementById('spin-legend-button');
const charactersResult = document.getElementById('characters-result');
const worldBossResult = document.getElementById('world-boss-result');
const weeklyBossResult = document.getElementById('weekly-boss-result');
const legendResult = document.getElementById('legend-result');
const charactersPlaceholder = document.getElementById('characters-placeholder');
const worldBossPlaceholder = document.getElementById('world-boss-placeholder');
const weeklyBossPlaceholder = document.getElementById('weekly-boss-placeholder');
const legendPlaceholder = document.getElementById('legend-placeholder');
const charactersLoader = document.getElementById('characters-loader');
const worldBossLoader = document.getElementById('world-boss-loader');
const weeklyBossLoader = document.getElementById('weekly-boss-loader');
const legendLoader = document.getElementById('legend-loader');
const historyContent = document.getElementById('history-content');
const historyPlaceholder = document.getElementById('history-placeholder');
const clearHistoryButton = document.getElementById('clear-history-button');
const showFullHistoryButton = document.getElementById('show-full-history-button');
const statsToggle = document.getElementById('stats-toggle');
const statsPanel = document.getElementById('stats-panel');
const settingsToggle = document.getElementById('settings-toggle');
const helpToggle = document.getElementById('help-toggle');
const helpModal = document.getElementById('help-modal');
const helpModalClose = document.getElementById('help-modal-close');
const settingsPanel = document.getElementById('settings-panel');
const teamSizeSelect = document.getElementById('team-size');
const animationSpeedSelect = document.getElementById('animation-speed');
const duplicatesSelect = document.getElementById('duplicates');
const soundToggle = document.getElementById('sound-toggle');
const animationsToggle = document.getElementById('animations-toggle');
const confirmModal = document.getElementById('confirm-modal');
const confirmModalContent = confirmModal.querySelector('.modal-content');
const confirmModalMessage = document.getElementById('confirm-modal-message');
const confirmModalCancel = document.getElementById('confirm-modal-cancel');
const confirmModalOk = document.getElementById('confirm-modal-ok');
const playerCharactersSelection = document.getElementById('player-characters-selection');
const characterSearchInput = document.getElementById('character-search');
const elementFilterSelect = document.getElementById('element-filter');
const clearPlayerCharactersButton = document.getElementById('clear-player-characters');
const selectAllCharactersButton = document.getElementById('select-all-characters');
const saveTeamSection = document.getElementById('save-characters-section');
const teamNameInput = document.getElementById('team-name-input');
const saveTeamButton = document.getElementById('save-team-button');
const shareTeamButton = document.getElementById('share-team-button');
const saveWorldBossSection = document.getElementById('save-world-boss-section');
const saveWorldBossButton = document.getElementById('save-world-boss-button');
const saveWeeklyBossSection = document.getElementById('save-weekly-boss-section');
const saveWeeklyBossButton = document.getElementById('save-weekly-boss-button');
const saveLegendSection = document.getElementById('save-legend-section');
const saveLegendButton = document.getElementById('save-legend-button');
const spinSound = document.getElementById('spin-sound');
const revealSound = document.getElementById('reveal-sound');
const clickSound = document.getElementById('click-sound');
const successSound = document.getElementById('success-sound');
const charactersSection = document.getElementById('characters-section');
const worldBossesSection = document.getElementById('world-bosses-section');
const weeklyBossesSection = document.getElementById('weekly-bosses-section');
const localLegendsSection = document.getElementById('local-legends-section');
const bossesSettingsContainer = document.getElementById('bosses-settings-container');
const enableAllBossesButton = document.getElementById('enable-all-bosses');
const disableAllBossesButton = document.getElementById('disable-all-bosses');
const resetBossesButton = document.getElementById('reset-bosses');
const selectedCountElement = document.getElementById('selected-count');
const selectionProgressBar = document.getElementById('selection-progress-bar');
const selectionPercentage = document.getElementById('selection-percentage');
const ownedCharactersElement = document.getElementById('owned-characters');
const currentTeamSizeElement = document.getElementById('current-team-size');
const availableWorldBossesElement = document.getElementById('available-world-bosses');
const availableWeeklyBossesElement = document.getElementById('available-weekly-bosses');
const availableLegendsElement = document.getElementById('available-legends');
const historyTotalElement = document.getElementById('history-total');
const historyTeamsElement = document.getElementById('history-teams');
const historyBossesElement = document.getElementById('history-bosses');
const historyLegendsElement = document.getElementById('history-legends');
const backToTopButton = document.getElementById('back-to-top');
const exportHistoryButton = document.getElementById('export-history-button');
// Full History Modal elements
const fullHistoryModal = document.getElementById('full-history-modal');
const fullHistoryModalContent = fullHistoryModal.querySelector('.modal-content');
const fullHistoryModalContentInner = document.getElementById('full-history-modal-content-inner');
const fullHistoryModalCloseButton = document.getElementById('full-history-modal-close');
const fullHistoryModalCloseBottomButton = document.getElementById('full-history-modal-close-bottom');
// ==================== UTILITY FUNCTIONS ====================
function playSound(soundName) {
if (!config.soundEnabled) return;
const soundMap = {
'spin': spinSound,
'reveal': revealSound,
'click': clickSound,
'success': successSound
};
const sound = soundMap[soundName];
if (sound) {
sound.currentTime = 0;
sound.play().catch(e => console.log("Audio play failed:", e));
}
}
function createRipple(event) {
if (!config.animationsEnabled) return;
const button = event.currentTarget;
const circle = document.createElement('span');
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - button.getBoundingClientRect().left - radius}px`;
circle.style.top = `${event.clientY - button.getBoundingClientRect().top - radius}px`;
circle.classList.add('ripple');
const ripple = button.getElementsByClassName('ripple')[0];
if (ripple) ripple.remove();
button.appendChild(circle);
}
function createParticles(element, count = 20, color = '#667eea') {
if (!config.animationsEnabled) return;
for (let i = 0; i < count; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.backgroundColor = color;
particle.style.setProperty('--tx', `${Math.random() * 100 - 50}px`);
particle.style.setProperty('--ty', `${Math.random() * 100 - 50}px`);
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
element.appendChild(particle);
setTimeout(() => particle.remove(), 2000);
}
}
function createConfetti(count = 100) {
if (!config.animationsEnabled) return;
const colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe', '#00f2fe', '#f6d365', '#fda085'];
for (let i = 0; i < count; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.setProperty('--bg', colors[Math.floor(Math.random() * colors.length)]);
confetti.style.left = `${Math.random() * 100}%`;
confetti.style.width = `${Math.random() * 10 + 5}px`;
confetti.style.height = `${Math.random() * 10 + 5}px`;
confetti.style.animation = `confetti-fall ${Math.random() * 3 + 2}s linear forwards`;
document.body.appendChild(confetti);
setTimeout(() => confetti.remove(), 5000);
}
}
function updateStats() {
// Update character stats
ownedCharactersElement.textContent = playerCharacters.length;
document.getElementById('owned-characters').textContent = playerCharacters.length;
// Update boss stats
const availableWorldBosses = WORLD_BOSSES.filter(boss =>
!config.excludedBosses['world-bosses'].includes(boss.name)
).length;
const availableWeeklyBosses = WEEKLY_BOSSES.filter(boss =>
!config.excludedBosses['weekly-bosses'].includes(boss.name)
).length;
const availableLegends = LOCAL_LEGENDS.filter(legend =>
!config.excludedBosses['local-legends'].includes(legend.name)
).length;
availableWorldBossesElement.textContent = availableWorldBosses;
availableWeeklyBossesElement.textContent = availableWeeklyBosses;
availableLegendsElement.textContent = availableLegends;
// Update history stats
const totalHistory = historyTeams.length + historyBosses.length;
const teamHistory = historyTeams.length;
const bossHistory = historyBosses.filter(item => item.type !== 'legend').length;
const legendHistory = historyBosses.filter(item => item.type === 'legend').length;
historyTotalElement.textContent = totalHistory;
historyTeamsElement.textContent = teamHistory;
historyBossesElement.textContent = bossHistory;
historyLegendsElement.textContent = legendHistory;
}
function updateSelectionProgress() {
const selectedCount = playerCharacters.length;
const totalCount = allCharacters.length;
const percentage = totalCount > 0 ? Math.round((selectedCount / totalCount) * 100) : 0;
selectedCountElement.querySelector('span:last-child').textContent = `${selectedCount}/${totalCount}`;
selectionProgressBar.style.width = `${percentage}%`;
selectionPercentage.textContent = `${percentage}%`;
if (selectedCount > 0) {
clearPlayerCharactersButton.classList.remove('hidden');
} else {
clearPlayerCharactersButton.classList.add('hidden');
}
}
// ==================== TAB SYSTEM ====================
function switchTab(category) {
// Update tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('tab-active', 'gradient-primary', 'text-white');
btn.classList.add('glass');
});
// Activate clicked tab
const activeTab = document.querySelector(`.tab-btn[data-category="${category}"]`);
activeTab.classList.remove('glass');
activeTab.classList.add('tab-active', 'gradient-primary', 'text-white');
// Show corresponding section
const sections = {
'characters': charactersSection,
'world-bosses': worldBossesSection,
'weekly-bosses': weeklyBossesSection,
'local-legends': localLegendsSection
};
// Hide all sections
Object.values(sections).forEach(section => {
section.classList.add('hidden');
});
// Show selected section with animation
const selectedSection = sections[category];
selectedSection.classList.remove('hidden');
if (config.animationsEnabled) {
selectedSection.style.opacity = '0';
selectedSection.style.transform = 'translateY(20px)';
requestAnimationFrame(() => {
selectedSection.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
selectedSection.style.opacity = '1';
selectedSection.style.transform = 'translateY(0)';
});
}
// Update config
config.category = category;
localStorage.setItem('genshinCategory', category);
playSound('click');
}
// ==================== CHARACTER CARD CREATION ====================
function createCharacterCard(character, isSelectable = false, isSelected = false) {
const card = document.createElement('div');
card.className = `character-card glass rounded-2xl p-4 text-center relative overflow-hidden ${isSelectable ? 'cursor-pointer' : ''}`;
if (isSelected) {
card.classList.add('selected');
}
const elementName = translations[currentLanguage][`elements.${character.element.toLowerCase()}`] || character.element;
card.innerHTML = `
<div class="relative mb-3 mx-auto character-avatar-lg">
<img src="${character.icon}" alt="${character.name}"
class="w-full h-full rounded-full object-cover border-4 ${isSelected ? 'border-purple-500' : 'border-gray-700'}">
${character.element ? `
<div class="element-icon tooltip" data-tooltip="${elementName}">
<img src="pic/Elements/Element_${character.element}.webp"
alt="${character.element}"
class="w-full h-full rounded-full">
</div>
` : ''}
${isSelectable && isSelected ? `
<div class="absolute top-0 right-0 w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center">
<i class="fas fa-check text-white text-xs"></i>
</div>
` : ''}
</div>
<p class="font-semibold text-sm text-white truncate">${character.name}</p>
`;
if (isSelectable) {
card.addEventListener('click', function(e) {
if (!e.target.closest('.tooltip')) {
togglePlayerCharacter(character);
playSound('click');
}
});
}
return card;
}
function createBossCard(boss, isLegend = false) {
const card = document.createElement('div');
card.className = `glass rounded-2xl p-6 text-center card-hover ${isLegend ? 'border-l-4 border-purple-500' : 'border-l-4 border-blue-500'}`;
const isClickable = boss.guideUrl && boss.guideUrl !== "#";
card.innerHTML = `
<img src="${boss.icon}" alt="${boss.name}"
class="boss-avatar rounded-full object-cover mx-auto mb-4 border-4 ${isLegend ? 'border-purple-500' : 'border-blue-500'}">
<h3 class="font-bold text-lg mb-2 text-white">${boss.name}</h3>
${isClickable ? `
<a href="${boss.guideUrl}" target="_blank"
class="inline-block mt-3 px-4 py-2 rounded-xl gradient-secondary text-white text-sm font-semibold hover:scale-105 transition-transform">
<i class="fas fa-external-link-alt mr-2"></i>
<span data-i18n="buttons.viewGuide">View Guide</span>
</a>
` : ''}
`;
return card;
}
// ==================== CHARACTER SELECTION ====================
function renderPlayerCharactersSelection(filter = '', elementFilter = 'all') {
playerCharactersSelection.innerHTML = '';
let filteredCharacters = allCharacters.filter(char => {
const nameMatch = char.name.toLowerCase().includes(filter.toLowerCase());
const elementMatch = elementFilter === 'all' || char.element === elementFilter;
return nameMatch && elementMatch;
});
// Sort by selected first, then by name
filteredCharacters.sort((a, b) => {
const aSelected = playerCharacters.some(c => c.name === a.name);
const bSelected = playerCharacters.some(c => c.name === b.name);
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
return a.name.localeCompare(b.name);
});
filteredCharacters.forEach(char => {
const isSelected = playerCharacters.some(c => c.name === char.name);
playerCharactersSelection.appendChild(createCharacterCard(char, true, isSelected));
});
updateSelectionProgress();
}
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, elementFilterSelect.value);
updateStats();
}
function selectAllCharacters() {
playerCharacters = [...allCharacters];
localStorage.setItem(`${currentUserUID}_playerCharacters`, JSON.stringify(playerCharacters));
renderPlayerCharactersSelection(characterSearchInput.value, elementFilterSelect.value);
updateStats();
playSound('success');
}
function clearPlayerCharacters() {
playerCharacters = [];
localStorage.removeItem(`${currentUserUID}_playerCharacters`);
renderPlayerCharactersSelection(characterSearchInput.value, elementFilterSelect.value);
updateStats();
playSound('click');
}
// ==================== BOSS SETTINGS ====================
function renderBossSettings(category = 'world-bosses') {
bossesSettingsContainer.innerHTML = '';
let bosses = [];
let categoryName = '';
switch(category) {
case 'world-bosses':
bosses = WORLD_BOSSES;
categoryName = translations[currentLanguage]['settings.categories.worldBosses'];
break;
case 'weekly-bosses':
bosses = WEEKLY_BOSSES;
categoryName = translations[currentLanguage]['settings.categories.weeklyBosses'];
break;
case 'local-legends':
bosses = LOCAL_LEGENDS;
categoryName = translations[currentLanguage]['settings.categories.localLegends'];
break;
}
// Update active boss category button
document.querySelectorAll('.boss-category-btn').forEach(btn => {
btn.classList.remove('active', 'gradient-primary', 'text-white');
btn.classList.add('glass');
});
const activeBtn = document.querySelector(`.boss-category-btn[data-boss-category="${category}"]`);
activeBtn.classList.remove('glass');
activeBtn.classList.add('active', 'gradient-primary', 'text-white');
// Render boss toggles
bosses.forEach(boss => {
const isExcluded = config.excludedBosses[category].includes(boss.name);
const bossToggle = document.createElement('div');
bossToggle.className = 'card-gradient p-4 rounded-2xl flex items-center justify-between';
bossToggle.innerHTML = `
<div class="flex items-center">
<div class="w-16 h-16 rounded-full overflow-hidden mr-4 flex-shrink-0">
<img src="${boss.icon}" alt="${boss.name}" class="w-full h-full object-cover">
</div>
<div>
<h4 class="font-semibold text-white text-sm">${boss.name}</h4>
<p class="text-xs text-gray-400">${categoryName}</p>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox" class="boss-toggle-checkbox" ${isExcluded ? '' : 'checked'}>
<span class="toggle-slider"></span>
</label>
`;
const checkbox = bossToggle.querySelector('.boss-toggle-checkbox');
checkbox.addEventListener('change', (e) => {
const excludedList = config.excludedBosses[category];
if (e.target.checked) {
// Remove from excluded
const index = excludedList.indexOf(boss.name);
if (index > -1) excludedList.splice(index, 1);
} else {
// Add to excluded
if (!excludedList.includes(boss.name)) {
excludedList.push(boss.name);
}
}
localStorage.setItem(`${currentUserUID}_excludedBosses`, JSON.stringify(config.excludedBosses));
updateStats();
playSound('click');
});
bossesSettingsContainer.appendChild(bossToggle);
});
}
function enableAllBosses() {
const activeCategory = document.querySelector('.boss-category-btn.active').dataset.bossCategory;
config.excludedBosses[activeCategory] = [];
localStorage.setItem(`${currentUserUID}_excludedBosses`, JSON.stringify(config.excludedBosses));
renderBossSettings(activeCategory);
updateStats();
playSound('success');
}
function disableAllBosses() {
const activeCategory = document.querySelector('.boss-category-btn.active').dataset.bossCategory;
let bosses = [];
switch(activeCategory) {
case 'world-bosses': bosses = WORLD_BOSSES; break;
case 'weekly-bosses': bosses = WEEKLY_BOSSES; break;
case 'local-legends': bosses = LOCAL_LEGENDS; break;
}
config.excludedBosses[activeCategory] = bosses.map(boss => boss.name);
localStorage.setItem(`${currentUserUID}_excludedBosses`, JSON.stringify(config.excludedBosses));
renderBossSettings(activeCategory);
updateStats();
playSound('click');
}
function resetBosses() {
config.excludedBosses = {
'world-bosses': [],
'weekly-bosses': [],
'local-legends': []
};
localStorage.removeItem(`${currentUserUID}_excludedBosses`);
renderBossSettings('world-bosses');
updateStats();
playSound('click');
}
// ==================== SPIN ANIMATIONS ====================
async function spinCharacters() {
if (isSpinning) return;
const availableCharsForSpin = playerCharacters.length > 0 ? playerCharacters : allCharacters;
const teamSize = parseInt(teamSizeSelect.value);
if (availableCharsForSpin.length < teamSize) {
alert(translations[currentLanguage]['sections.characters.notEnough'] || `Not enough characters to form a team of ${teamSize}. Available: ${availableCharsForSpin.length}`);
return;
}
isSpinning = true;
spinCharactersButton.disabled = true;
spinCharactersButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Spinning...';
charactersResult.innerHTML = '';
charactersPlaceholder.classList.add('hidden');
charactersLoader.classList.remove('hidden');
saveTeamSection.classList.add('hidden');
playSound('spin');
let charsPool = config.allowDuplicates ? [...availableCharsForSpin] : [...availableCharsForSpin];
const selectedTeam = [];
// Determine spin duration based on speed setting
let spinDurationMs;
switch (animationSpeedSelect.value) {
case 'fast': spinDurationMs = 1000; break;
case 'slow': spinDurationMs = 4000; break;
default: spinDurationMs = 2000; break;
}
const individualSpinTime = spinDurationMs / teamSize;
// Create spinning placeholders
for (let i = 0; i < teamSize; i++) {
const placeholder = document.createElement('div');
placeholder.className = 'flex flex-col items-center p-4';
placeholder.innerHTML = `
<div class="w-24 h-24 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center mb-3">
<i class="fas fa-question text-white text-3xl"></i>
</div>
<div class="w-32 h-4 bg-gradient-to-r from-gray-600 to-gray-700 rounded-full"></div>
`;
charactersResult.appendChild(placeholder);
}
// Perform spin animation
for (let i = 0; i < teamSize; i++) {
await new Promise(resolve => setTimeout(resolve, individualSpinTime));
let selectedChar;
if (config.allowDuplicates) {
selectedChar = charsPool[Math.floor(Math.random() * charsPool.length)];
} else {
const selectedIndex = Math.floor(Math.random() * charsPool.length);
selectedChar = charsPool.splice(selectedIndex, 1)[0];
}
selectedTeam.push(selectedChar);
}
// Show results
charactersLoader.classList.add('hidden');
charactersResult.innerHTML = '';
// Create a grid container for better layout
const gridContainer = document.createElement('div');
gridContainer.className = 'grid grid-cols-2 md:grid-cols-4 gap-6 w-full';
for (let i = 0; i < selectedTeam.length; i++) {
const char = selectedTeam[i];
const card = createCharacterCard(char);
card.classList.add('reveal-animation');
card.style.animationDelay = `${i * 0.1}s`;
gridContainer.appendChild(card);
createParticles(card, 10, '#667eea');
}
charactersResult.appendChild(gridContainer);
playSound('reveal');
createConfetti(50);
// Enable button and show save section
spinCharactersButton.disabled = false;
spinCharactersButton.innerHTML = '<i class="fas fa-dice-d20 mr-3"></i> Spin Team!';
isSpinning = false;
saveTeamSection.classList.remove('hidden');
shareTeamButton.classList.remove('hidden');
teamNameInput.value = '';
currentGeneratedTeam = selectedTeam;
// Update current team size display
currentTeamSizeElement.textContent = teamSize;
}
async function spinWorldBoss() {
if (isSpinning) return;
isSpinning = true;
spinWorldBossButton.disabled = true;
spinWorldBossButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Spinning...';
worldBossResult.innerHTML = '';
worldBossPlaceholder.classList.add('hidden');
worldBossLoader.classList.remove('hidden');
saveWorldBossSection.classList.add('hidden');
playSound('spin');
const availableBosses = WORLD_BOSSES.filter(boss =>
!config.excludedBosses['world-bosses'].includes(boss.name)
);
if (availableBosses.length === 0) {
worldBossResult.innerHTML = `
<div class="text-center p-8">
<div class="text-5xl mb-4">⚠️</div>
<h3 class="text-xl font-bold text-white mb-2">No Bosses Available</h3>
<p class="text-gray-400">Enable some bosses in the settings to spin!</p>
</div>
`;
worldBossLoader.classList.add('hidden');
spinWorldBossButton.disabled = false;
spinWorldBossButton.innerHTML = '<i class="fas fa-dragon mr-3"></i> Spin Boss!';
isSpinning = false;
return;
}
await new Promise(resolve => setTimeout(resolve, 1500));
const randomBoss = availableBosses[Math.floor(Math.random() * availableBosses.length)];
worldBossLoader.classList.add('hidden');
worldBossResult.innerHTML = '';
const card = createBossCard(randomBoss, false);
card.classList.add('reveal-animation');
worldBossResult.appendChild(card);
playSound('reveal');
createParticles(card, 15, '#4facfe');
spinWorldBossButton.disabled = false;
spinWorldBossButton.innerHTML = '<i class="fas fa-dragon mr-3"></i> Spin Boss!';
isSpinning = false;
saveWorldBossSection.classList.remove('hidden');
currentGeneratedBoss = randomBoss;
isWorldBoss = true;
isLegend = false;
}
async function spinWeeklyBoss() {
if (isSpinning) return;
isSpinning = true;
spinWeeklyBossButton.disabled = true;
spinWeeklyBossButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Spinning...';
weeklyBossResult.innerHTML = '';
weeklyBossPlaceholder.classList.add('hidden');
weeklyBossLoader.classList.remove('hidden');
saveWeeklyBossSection.classList.add('hidden');
playSound('spin');
const availableBosses = WEEKLY_BOSSES.filter(boss =>
!config.excludedBosses['weekly-bosses'].includes(boss.name)
);
if (availableBosses.length === 0) {
weeklyBossResult.innerHTML = `
<div class="text-center p-8">
<div class="text-5xl mb-4">⚠️</div>
<h3 class="text-xl font-bold text-white mb-2">No Bosses Available</h3>
<p class="text-gray-400">Enable some bosses in the settings to spin!</p>
</div>
`;
weeklyBossLoader.classList.add('hidden');
spinWeeklyBossButton.disabled = false;
spinWeeklyBossButton.innerHTML = '<i class="fas fa-crown mr-3"></i> Spin Weekly Boss!';
isSpinning = false;
return;
}
await new Promise(resolve => setTimeout(resolve, 1500));
const randomBoss = availableBosses[Math.floor(Math.random() * availableBosses.length)];
weeklyBossLoader.classList.add('hidden');
weeklyBossResult.innerHTML = '';
const card = createBossCard(randomBoss, false);
card.classList.add('reveal-animation');
weeklyBossResult.appendChild(card);
playSound('reveal');
createParticles(card, 15, '#f6d365');
spinWeeklyBossButton.disabled = false;
spinWeeklyBossButton.innerHTML = '<i class="fas fa-crown mr-3"></i> Spin Weekly Boss!';
isSpinning = false;
saveWeeklyBossSection.classList.remove('hidden');
currentGeneratedBoss = randomBoss;
isWorldBoss = false;
isLegend = false;
}
async function spinLocalLegend() {
if (isSpinning) return;
isSpinning = true;
spinLegendButton.disabled = true;
spinLegendButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Spinning...';
legendResult.innerHTML = '';
legendPlaceholder.classList.add('hidden');
legendLoader.classList.remove('hidden');
saveLegendSection.classList.add('hidden');
playSound('spin');
const availableLegends = LOCAL_LEGENDS.filter(legend =>
!config.excludedBosses['local-legends'].includes(legend.name)
);
if (availableLegends.length === 0) {
legendResult.innerHTML = `
<div class="text-center p-8">
<div class="text-5xl mb-4">⚠️</div>
<h3 class="text-xl font-bold text-white mb-2">No Legends Available</h3>
<p class="text-gray-400">Enable some legends in the settings to spin!</p>
</div>
`;
legendLoader.classList.add('hidden');
spinLegendButton.disabled = false;
spinLegendButton.innerHTML = '<i class="fas fa-landmark mr-3"></i> Spin Legend!';
isSpinning = false;
return;
}
await new Promise(resolve => setTimeout(resolve, 1500));
const randomLegend = availableLegends[Math.floor(Math.random() * availableLegends.length)];
legendLoader.classList.add('hidden');
legendResult.innerHTML = '';
const card = createBossCard(randomLegend, true);
card.classList.add('reveal-animation');
legendResult.appendChild(card);
playSound('reveal');
createParticles(card, 15, '#a8edea');
spinLegendButton.disabled = false;
spinLegendButton.innerHTML = '<i class="fas fa-landmark mr-3"></i> Spin Legend!';
isSpinning = false;
saveLegendSection.classList.remove('hidden');
currentGeneratedBoss = randomLegend;
isWorldBoss = false;
isLegend = true;
}
// ==================== HISTORY SYSTEM ====================
function saveTeamToHistory(team, name = '') {
const teamData = {
date: new Date().toISOString(),
characters: team,
name: name,
type: 'team'
};
historyTeams.unshift(teamData);
localStorage.setItem(`${currentUserUID}_genshinTeamsHistory`, JSON.stringify(historyTeams));
renderHistory();
updateStats();
playSound('success');
}
function saveBossToHistory(boss, isWorld, isLegendType = false) {
const bossData = {
date: new Date().toISOString(),
name: boss.name,
icon: boss.icon,
type: isLegendType ? 'legend' : (isWorld ? 'world-boss' : 'weekly-boss'),
guideUrl: boss.guideUrl || null
};
historyBosses.unshift(bossData);
localStorage.setItem(`${currentUserUID}_genshinBossesHistory`, JSON.stringify(historyBosses));
renderHistory();
updateStats();
playSound('success');
}
function renderHistory(filter = 'all') {
historyContent.innerHTML = '';
const allHistory = [...historyTeams, ...historyBosses]
.sort((a, b) => new Date(b.date) - new Date(a.date))
.filter(item => {
if (filter === 'all') return true;
if (filter === 'team') return item.type === 'team';
if (filter === 'world-boss') return item.type === 'world-boss';
if (filter === 'weekly-boss') return item.type === 'weekly-boss';
if (filter === 'legend') return item.type === 'legend';
return true;
})
.slice(0, 6); // Show only 6 most recent items
if (allHistory.length === 0) {
historyPlaceholder.classList.remove('hidden');
clearHistoryButton.classList.add('hidden');
showFullHistoryButton.classList.add('hidden');
} else {
historyPlaceholder.classList.add('hidden');
clearHistoryButton.classList.remove('hidden');
showFullHistoryButton.classList.remove('hidden');
allHistory.forEach((item, index) => {
const historyItem = document.createElement('div');
historyItem.className = `history-item glass rounded-2xl p-6 ${item.type}`;
let title = '';
let content = '';
let icon = '';
let badgeClass = '';
let badgeText = '';
if (item.type === 'team') {
const teamName = item.name ? `"${item.name}"` : 'Unnamed Team';
title = `${teamName} - ${new Date(item.date).toLocaleString()}`;
badgeClass = 'badge-team';
badgeText = translations[currentLanguage]['history.filters.teams'];
icon = '<i class="fas fa-users mr-2"></i>';
content = `
<div class="flex flex-wrap gap-3 mt-4">
${item.characters.map(char => `
<div class="flex flex-col items-center">
<img src="${char.icon}" alt="${char.name}"
class="w-16 h-16 rounded-full object-cover border-2 border-gray-700">
<span class="text-xs mt-1 text-gray-300">${char.name}</span>
</div>
`).join('')}
</div>
`;
} else {
const typeText = item.type === 'legend' ?
translations[currentLanguage]['history.filters.legends'] :
(item.type === 'world-boss' ?
translations[currentLanguage]['history.filters.worldBosses'] :
translations[currentLanguage]['history.filters.weeklyBosses']);
title = `${item.name} - ${new Date(item.date).toLocaleString()}`;
badgeClass = item.type === 'legend' ? 'badge-legend' :
(item.type === 'world-boss' ? 'badge-boss' : 'badge-team');
badgeText = typeText;
icon = item.type === 'legend' ? '<i class="fas fa-landmark mr-2"></i>' :
(item.type === 'world-boss' ? '<i class="fas fa-dragon mr-2"></i>' : '<i class="fas fa-crown mr-2"></i>');
content = `
<div class="flex items-center justify-center mt-4">
<img src="${item.icon}" alt="${item.name}"
class="w-24 h-24 rounded-full object-cover border-4 ${item.type === 'legend' ? 'border-purple-500' : 'border-blue-500'}">
</div>
`;
}
historyItem.innerHTML = `
<div class="flex justify-between items-start mb-4">
<div>
<div class="flex items-center mb-2">
<span class="${badgeClass} badge flex items-center text-xs">
${icon}${badgeText}
</span>
</div>
<h3 class="font-bold text-white">${title}</h3>
</div>
<button class="delete-history-item p-2 rounded-lg glass hover:bg-red-500 hover:text-white transition-colors" data-index="${index}" data-type="${item.type}">
<i class="fas fa-times"></i>
</button>
</div>
${content}
`;
historyContent.appendChild(historyItem);
// Add delete functionality
const deleteBtn = historyItem.querySelector('.delete-history-item');
deleteBtn.addEventListener('click', function() {
const idx = parseInt(this.dataset.index);
const type = this.dataset.type;
if (type === 'team') {
historyTeams.splice(idx, 1);
localStorage.setItem(`${currentUserUID}_genshinTeamsHistory`, JSON.stringify(historyTeams));
} else {
historyBosses.splice(idx, 1);
localStorage.setItem(`${currentUserUID}_genshinBossesHistory`, JSON.stringify(historyBosses));
}
renderHistory(filter);
updateStats();
playSound('click');
});
});
}
}
function clearHistory() {
historyTeams = [];
historyBosses = [];
localStorage.removeItem(`${currentUserUID}_genshinTeamsHistory`);
localStorage.removeItem(`${currentUserUID}_genshinBossesHistory`);
renderHistory();
updateStats();
hideConfirmModal();
playSound('success');
}
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 = `
<div class="text-center py-12">
<div class="text-5xl mb-4">📜</div>
<h3 class="text-xl font-bold text-white mb-2">No History</h3>
<p class="text-gray-400">Your history will appear here after you save some results.</p>
</div>
`;
} else {
allHistory.forEach((item, index) => {
const historyItem = document.createElement('div');
historyItem.className = `glass rounded-2xl p-6 mb-4 ${item.type}`;
let title = '';
let content = '';
let icon = '';
let badgeClass = '';
let badgeText = '';
if (item.type === 'team') {
const teamName = item.name ? `"${item.name}"` : 'Unnamed Team';
title = `${teamName}`;
badgeClass = 'badge-team';
badgeText = translations[currentLanguage]['history.filters.teams'];
icon = '<i class="fas fa-users mr-2"></i>';
content = `
<div class="flex overflow-x-auto gap-3 mt-4 pb-2 custom-scrollbar">
${item.characters.map(char => `
<div class="flex-shrink-0 flex flex-col items-center">
<img src="${char.icon}" alt="${char.name}"
class="w-20 h-20 rounded-full object-cover border-2 border-gray-700">
<span class="text-xs mt-1 text-gray-300 text-center">${char.name}</span>
</div>
`).join('')}
</div>
`;
} else {
const typeText = item.type === 'legend' ?
translations[currentLanguage]['history.filters.legends'] :
(item.type === 'world-boss' ?
translations[currentLanguage]['history.filters.worldBosses'] :
translations[currentLanguage]['history.filters.weeklyBosses']);
title = `${item.name}`;
badgeClass = item.type === 'legend' ? 'badge-legend' :
(item.type === 'world-boss' ? 'badge-boss' : 'badge-team');
badgeText = typeText;
icon = item.type === 'legend' ? '<i class="fas fa-landmark mr-2"></i>' :
(item.type === 'world-boss' ? '<i class="fas fa-dragon mr-2"></i>' : '<i class="fas fa-crown mr-2"></i>');
content = `
<div class="flex items-center justify-center mt-4">
<img src="${item.icon}" alt="${item.name}"
class="w-28 h-28 rounded-full object-cover border-4 ${item.type === 'legend' ? 'border-purple-500' : 'border-blue-500'}">
</div>
`;
}
const date = new Date(item.date).toLocaleString();
historyItem.innerHTML = `
<div class="flex justify-between items-start mb-4">
<div>
<div class="flex items-center mb-2">
<span class="${badgeClass} badge flex items-center text-xs">
${icon}${badgeText}
</span>
</div>
<h3 class="font-bold text-white">${title}</h3>
<p class="text-sm text-gray-400 mt-1">${date}</p>
</div>
</div>
${content}
`;
fullHistoryModalContentInner.appendChild(historyItem);
});
}
fullHistoryModal.classList.remove('hidden');
setTimeout(() => {
fullHistoryModalContent.classList.add('modal-content');
}, 10);
}
function hideFullHistoryModal() {
fullHistoryModal.classList.add('hidden');
fullHistoryModalContent.classList.remove('modal-content');
}
function exportHistory() {
const allHistory = [...historyTeams, ...historyBosses]
.sort((a, b) => new Date(b.date) - new Date(a.date));
const exportData = {
exportedAt: new Date().toISOString(),
uid: currentUserUID,
history: allHistory
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = `genshin-roulette-history-${new Date().toISOString().split('T')[0]}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
playSound('success');
}
// ==================== MODAL FUNCTIONS ====================
function showConfirmModal(message, callback) {
confirmModalMessage.textContent = message;
confirmModal.classList.remove('hidden');
const handleConfirm = () => {
callback();
confirmModalOk.removeEventListener('click', handleConfirm);
confirmModalCancel.removeEventListener('click', handleCancel);
};
const handleCancel = () => {
hideConfirmModal();
confirmModalOk.removeEventListener('click', handleConfirm);
confirmModalCancel.removeEventListener('click', handleCancel);
};
confirmModalOk.addEventListener('click', handleConfirm);
confirmModalCancel.addEventListener('click', handleCancel);
playSound('click');
}
function hideConfirmModal() {
confirmModal.classList.add('hidden');
}
function showHelpModal() {
helpModal.classList.remove('hidden');
playSound('click');
}
function hideHelpModal() {
helpModal.classList.add('hidden');
playSound('click');
}
// ==================== EVENT LISTENERS ====================
// Stats toggle
statsToggle.addEventListener('click', () => {
statsPanel.classList.toggle('open');
playSound('click');
});
statsToggle.addEventListener('mousedown', createRipple);
// Settings toggle
settingsToggle.addEventListener('click', () => {
settingsPanel.classList.toggle('open');
playSound('click');
});
settingsToggle.addEventListener('mousedown', createRipple);
// Help toggle
helpToggle.addEventListener('click', showHelpModal);
helpToggle.addEventListener('mousedown', createRipple);
helpModalClose.addEventListener('click', hideHelpModal);
helpModal.addEventListener('click', (e) => {
if (e.target === helpModal) hideHelpModal();
});
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
switchTab(this.dataset.category);
});
btn.addEventListener('mousedown', createRipple);
});
// Boss category switching
document.querySelectorAll('.boss-category-btn').forEach(btn => {
btn.addEventListener('click', function() {
renderBossSettings(this.dataset.bossCategory);
});
btn.addEventListener('mousedown', createRipple);
});
// History filter switching
document.querySelectorAll('.history-filter-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.history-filter-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
renderHistory(this.dataset.historyFilter);
playSound('click');
});
});
// Spin buttons
spinCharactersButton.addEventListener('click', spinCharacters);
spinCharactersButton.addEventListener('mousedown', createRipple);
spinWorldBossButton.addEventListener('click', spinWorldBoss);
spinWorldBossButton.addEventListener('mousedown', createRipple);
spinWeeklyBossButton.addEventListener('click', spinWeeklyBoss);
spinWeeklyBossButton.addEventListener('mousedown', createRipple);
spinLegendButton.addEventListener('click', spinLocalLegend);
spinLegendButton.addEventListener('mousedown', createRipple);
// Save buttons
saveTeamButton.addEventListener('click', () => {
const teamName = teamNameInput.value.trim();
if (currentGeneratedTeam.length > 0) {
saveTeamToHistory(currentGeneratedTeam, teamName);
saveTeamSection.classList.add('hidden');
shareTeamButton.classList.add('hidden');
}
});
saveTeamButton.addEventListener('mousedown', createRipple);
saveWorldBossButton.addEventListener('click', () => {
if (currentGeneratedBoss) {
saveBossToHistory(currentGeneratedBoss, true);
saveWorldBossSection.classList.add('hidden');
}
});
saveWorldBossButton.addEventListener('mousedown', createRipple);
saveWeeklyBossButton.addEventListener('click', () => {
if (currentGeneratedBoss) {
saveBossToHistory(currentGeneratedBoss, false);
saveWeeklyBossSection.classList.add('hidden');
}
});
saveWeeklyBossButton.addEventListener('mousedown', createRipple);
saveLegendButton.addEventListener('click', () => {
if (currentGeneratedBoss) {
saveBossToHistory(currentGeneratedBoss, false, true);
saveLegendSection.classList.add('hidden');
}
});
saveLegendButton.addEventListener('mousedown', createRipple);
// History buttons
clearHistoryButton.addEventListener('click', () => {
showConfirmModal(
translations[currentLanguage]['modals.confirm.message'],
clearHistory
);
});
clearHistoryButton.addEventListener('mousedown', createRipple);
showFullHistoryButton.addEventListener('click', showFullHistoryModal);
showFullHistoryButton.addEventListener('mousedown', createRipple);
exportHistoryButton.addEventListener('click', exportHistory);
exportHistoryButton.addEventListener('mousedown', createRipple);
fullHistoryModalCloseButton.addEventListener('click', hideFullHistoryModal);
fullHistoryModalCloseBottomButton.addEventListener('click', hideFullHistoryModal);
fullHistoryModal.addEventListener('click', (e) => {
if (e.target === fullHistoryModal) hideFullHistoryModal();
});
// Character selection
characterSearchInput.addEventListener('input', (e) => {
renderPlayerCharactersSelection(e.target.value, elementFilterSelect.value);
});
elementFilterSelect.addEventListener('change', (e) => {
renderPlayerCharactersSelection(characterSearchInput.value, e.target.value);
});
clearPlayerCharactersButton.addEventListener('click', () => {
showConfirmModal(
translations[currentLanguage]['settings.clearSelected'] + '?',
clearPlayerCharacters
);
});
clearPlayerCharactersButton.addEventListener('mousedown', createRipple);
selectAllCharactersButton.addEventListener('click', selectAllCharacters);
selectAllCharactersButton.addEventListener('mousedown', createRipple);
// Boss settings
enableAllBossesButton.addEventListener('click', enableAllBosses);
enableAllBossesButton.addEventListener('mousedown', createRipple);
disableAllBossesButton.addEventListener('click', disableAllBosses);
disableAllBossesButton.addEventListener('mousedown', createRipple);
resetBossesButton.addEventListener('click', resetBosses);
resetBossesButton.addEventListener('mousedown', createRipple);
// Settings changes
teamSizeSelect.addEventListener('change', (e) => {
config.teamSize = parseInt(e.target.value);
localStorage.setItem(`${currentUserUID}_teamSize`, config.teamSize);
currentTeamSizeElement.textContent = e.target.value;
playSound('click');
});
animationSpeedSelect.addEventListener('change', (e) => {
config.animationSpeed = e.target.value;
localStorage.setItem(`${currentUserUID}_animationSpeed`, config.animationSpeed);
playSound('click');
});
duplicatesSelect.addEventListener('change', (e) => {
config.allowDuplicates = e.target.value === 'yes';
localStorage.setItem('allowDuplicates', e.target.value);
playSound('click');
});
soundToggle.addEventListener('change', (e) => {
config.soundEnabled = e.target.checked;
localStorage.setItem('soundEnabled', config.soundEnabled);
playSound('click');
});
animationsToggle.addEventListener('change', (e) => {
config.animationsEnabled = e.target.checked;
localStorage.setItem('animationsEnabled', config.animationsEnabled);
playSound('click');
});
// Modals
confirmModal.addEventListener('click', (e) => {
if (e.target === confirmModal) hideConfirmModal();
});
confirmModalCancel.addEventListener('click', hideConfirmModal);
confirmModalOk.addEventListener('click', hideConfirmModal);
// Back to top
backToTopButton.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
playSound('click');
});
backToTopButton.addEventListener('mousedown', createRipple);
// Initialize settings from localStorage
teamSizeSelect.value = config.teamSize;
animationSpeedSelect.value = config.animationSpeed;
duplicatesSelect.value = config.allowDuplicates ? 'yes' : 'no';
soundToggle.checked = config.soundEnabled;
animationsToggle.checked = config.animationsEnabled;
// ==================== INITIALIZATION ====================
renderPlayerCharactersSelection('', 'all');
renderBossSettings('world-bosses');
renderHistory('all');
updateStats();
updateSelectionProgress();
// Set initial tab
switchTab(config.category);
// Add ripple effect to all buttons
document.querySelectorAll('button').forEach(button => {
if (!button.classList.contains('tooltip')) {
button.addEventListener('mousedown', createRipple);
}
});
// Add click sound to all interactive elements
document.querySelectorAll('button, select, input[type="checkbox"]').forEach(element => {
element.addEventListener('click', () => playSound('click'));
});
console.log('Genshin Impact Roulette v2.1.0 initialized successfully!');
});
</script>
</body>
</html>