| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes"> |
| <title>Genshin Nexus · BETA Опти</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); |
| :root { |
| --bg-main: #0a0c12; |
| --bg-side: #0f1117; |
| --bg-card: #1a1e2a; |
| --bg-card-hover: #252b38; |
| --accent: #8b5cf6; |
| --accent-glow: rgba(139, 92, 246, 0.5); |
| --border: #2d3340; |
| --text-main: #ffffff; |
| --text-dim: #a0a8c0; |
| --text-soft: #d1d5db; |
| --success: #10b981; |
| --warning: #f59e0b; |
| --error: #ef4444; |
| --pyro: #ff6b4a; --pyro-glow: rgba(255, 107, 74, 0.4); |
| --hydro: #3b82f6; --hydro-glow: rgba(59, 130, 246, 0.4); |
| --anemo: #34d399; --anemo-glow: rgba(52, 211, 153, 0.4); |
| --electro: #a855f7; --electro-glow: rgba(168, 85, 247, 0.4); |
| --dendro: #84cc16; --dendro-glow: rgba(132, 204, 22, 0.4); |
| --cryo: #38bdf8; --cryo-glow: rgba(56, 189, 248, 0.4); |
| --geo: #fbbf24; --geo-glow: rgba(251, 191, 36, 0.4); |
| --radius-xs: 4px; |
| --radius-sm: 6px; |
| --radius-md: 8px; |
| --radius-lg: 12px; |
| --radius-xl: 16px; |
| } |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| font-family: 'Inter', system-ui, sans-serif; |
| } |
| body { |
| background: radial-gradient(circle at 20% 30%, #121624, var(--bg-main)); |
| color: var(--text-main); |
| display: flex; |
| height: 100vh; |
| overflow: hidden; |
| } |
| ::-webkit-scrollbar { width: 6px; height: 6px; } |
| ::-webkit-scrollbar-track { background: var(--bg-side); } |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--accent); } |
| |
| |
| .blur-popup { |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| backdrop-filter: blur(16px) brightness(0.3); |
| -webkit-backdrop-filter: blur(16px) brightness(0.3); |
| background: rgba(0, 0, 0, 0.5); |
| z-index: 3000; |
| display: none; |
| align-items: center; |
| justify-content: center; |
| } |
| .blur-popup.active { display: flex; } |
| .popup-content { |
| background: linear-gradient(145deg, var(--bg-card), #151a26); |
| border: 1px solid rgba(255,255,255,0.1); |
| border-radius: var(--radius-xl); |
| padding: 28px; |
| max-width: 600px; |
| width: 90%; |
| max-height: 80vh; |
| overflow-y: auto; |
| box-shadow: 0 25px 50px -8px rgba(0,0,0,0.6); |
| position: relative; |
| } |
| .popup-close { |
| position: absolute; |
| top: 20px; |
| right: 24px; |
| font-size: 28px; |
| cursor: pointer; |
| color: var(--text-dim); |
| transition: 0.2s; |
| } |
| .popup-close:hover { color: #fff; } |
| |
| |
| @media (max-width: 1024px) { |
| aside { width: 80px; } |
| .side-header h1 { font-size: 0; background: none; -webkit-text-fill-color: #fff; } |
| .side-header h1::after { content: "N"; font-size: 24px; -webkit-text-fill-color: #fff; } |
| .side-header p { display: none; } |
| .nav-item span:last-child { display: none; } |
| .filter-section label { display: none; } |
| .filter-section select { display: none; } |
| .filter-section .btn { display: none; } |
| .filter-section .btn:last-child { |
| display: block; |
| width: 100%; |
| margin-top: 0; |
| padding: 12px 0; |
| } |
| .filter-section .btn:last-child span { display: none; } |
| .filter-section .btn:last-child::after { content: "📖"; font-size: 20px; } |
| .stats-strip { gap: 12px; } |
| .stat-unit b { font-size: 16px; } |
| .content-area { padding: 16px; } |
| .view-toggle-btn { padding: 6px 12px; font-size: 12px; } |
| .data-table th { font-size: 11px; padding: 8px; } |
| .data-row td { padding: 12px; } |
| .weapon-badge { font-size: 11px; padding: 4px 10px; } |
| .role-badge { font-size: 11px; padding: 4px 10px; } |
| } |
| |
| @media (max-width: 768px) { |
| body { flex-direction: column; } |
| aside { |
| width: 100%; |
| height: auto; |
| border-right: none; |
| border-bottom: 1px solid var(--border); |
| } |
| .nav-group { |
| display: flex; |
| padding: 8px; |
| overflow-x: auto; |
| } |
| .nav-item { |
| flex-shrink: 0; |
| margin-bottom: 0; |
| margin-right: 8px; |
| } |
| .filter-section { display: none; } |
| header { |
| flex-wrap: wrap; |
| height: auto; |
| padding: 16px; |
| } |
| .stats-strip { |
| flex-wrap: wrap; |
| justify-content: center; |
| } |
| .content-area { padding: 16px; } |
| .chars-grid { grid-template-columns: 1fr; } |
| .team-members { justify-content: center; } |
| .synergy-card { padding: 20px; } |
| .team-member { min-width: 90px; padding: 12px; } |
| .member-icon { width: 56px; height: 56px; font-size: 24px; } |
| } |
| |
| |
| aside { |
| width: 280px; |
| background: linear-gradient(145deg, var(--bg-side), #0a0c11); |
| border-right: 1px solid rgba(255,255,255,0.05); |
| display: flex; |
| flex-direction: column; |
| flex-shrink: 0; |
| box-shadow: 6px 0 24px rgba(0,0,0,0.4); |
| transition: 0.3s; |
| overflow-y: auto; |
| } |
| .side-header { |
| padding: 28px 20px; |
| border-bottom: 1px solid rgba(255,255,255,0.05); |
| background: radial-gradient(circle at 0% 50%, rgba(139,92,246,0.15), transparent); |
| } |
| .side-header h1 { |
| font-size: 24px; |
| font-weight: 800; |
| letter-spacing: -0.5px; |
| background: linear-gradient(135deg, #fff, var(--accent)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: 4px; |
| } |
| .side-header p { |
| color: var(--text-dim); |
| font-size: 12px; |
| } |
| .nav-group { |
| padding: 20px 16px; |
| flex-grow: 1; |
| overflow-y: auto; |
| } |
| .nav-item { |
| padding: 12px 18px; |
| border-radius: var(--radius-md); |
| cursor: pointer; |
| margin-bottom: 6px; |
| font-size: 15px; |
| font-weight: 600; |
| color: var(--text-dim); |
| transition: 0.2s; |
| display: flex; |
| align-items: center; |
| gap: 14px; |
| border: 1px solid transparent; |
| backdrop-filter: blur(4px); |
| } |
| .nav-item:hover { |
| background: rgba(255,255,255,0.06); |
| color: #fff; |
| border-color: rgba(255,255,255,0.1); |
| transform: translateX(4px); |
| } |
| .nav-item.active { |
| background: linear-gradient(135deg, var(--accent), #a78bfa); |
| color: #fff; |
| box-shadow: 0 6px 16px var(--accent-glow); |
| border: none; |
| } |
| |
| |
| .filter-section { |
| padding: 20px; |
| border-top: 1px solid rgba(255,255,255,0.05); |
| background: rgba(0,0,0,0.2); |
| } |
| .filter-section label { |
| display: block; |
| font-size: 11px; |
| text-transform: uppercase; |
| color: var(--text-dim); |
| margin-bottom: 6px; |
| letter-spacing: 1px; |
| font-weight: 700; |
| } |
| select { |
| width: 100%; |
| background: rgba(0,0,0,0.4); |
| border: 1px solid rgba(255,255,255,0.1); |
| color: #fff; |
| padding: 10px 14px; |
| border-radius: var(--radius-md); |
| font-size: 13px; |
| margin-bottom: 16px; |
| cursor: pointer; |
| transition: 0.2s; |
| appearance: none; |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a0a8c0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); |
| background-repeat: no-repeat; |
| background-position: right 14px center; |
| background-size: 14px; |
| } |
| select:hover { border-color: var(--accent); } |
| |
| |
| main { |
| flex-grow: 1; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| background: radial-gradient(circle at 80% 20%, rgba(139,92,246,0.1), transparent 60%); |
| } |
| header { |
| height: 70px; |
| background: rgba(15, 17, 23, 0.8); |
| backdrop-filter: blur(16px); |
| -webkit-backdrop-filter: blur(16px); |
| border-bottom: 1px solid rgba(255,255,255,0.05); |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 0 24px; |
| } |
| .stats-strip { display: flex; gap: 32px; } |
| .stat-unit { display: flex; flex-direction: column; } |
| .stat-unit small { |
| font-size: 11px; |
| color: var(--text-dim); |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| font-weight: 600; |
| } |
| .stat-unit b { |
| font-size: 20px; |
| font-weight: 800; |
| background: linear-gradient(135deg, #fff, #d6ccff); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| .content-area { |
| padding: 24px; |
| overflow-y: auto; |
| flex-grow: 1; |
| position: relative; |
| } |
| |
| |
| .data-table { |
| width: 100%; |
| border-collapse: separate; |
| border-spacing: 0 10px; |
| } |
| .data-table th { |
| text-align: left; |
| padding: 10px 16px; |
| font-size: 12px; |
| color: var(--text-soft); |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| font-weight: 700; |
| opacity: 0.9; |
| } |
| .data-row { |
| border-radius: var(--radius-lg); |
| transition: 0.2s; |
| position: relative; |
| background: rgba(26, 30, 42, 0.7); |
| backdrop-filter: blur(8px); |
| box-shadow: 0 10px 24px -6px rgba(0,0,0,0.5); |
| border: 1px solid rgba(255,255,255,0.05); |
| } |
| .data-row td { |
| padding: 16px; |
| vertical-align: middle; |
| } |
| .data-row:hover { |
| background: rgba(37, 43, 56, 0.8); |
| border-color: rgba(139,92,246,0.3); |
| } |
| |
| |
| .chars-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); |
| gap: 20px; |
| } |
| .char-card { |
| background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9)); |
| backdrop-filter: blur(10px); |
| border-radius: var(--radius-xl); |
| padding: 24px; |
| border: 1px solid rgba(255,255,255,0.08); |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| transition: 0.25s; |
| box-shadow: 0 18px 28px -8px rgba(0,0,0,0.6); |
| position: relative; |
| overflow: hidden; |
| } |
| .char-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 4px; |
| background: linear-gradient(90deg, var(--element-color), var(--element-glow), transparent); |
| opacity: 0.9; |
| } |
| .char-card:hover { |
| transform: translateY(-4px); |
| border-color: var(--element-color); |
| box-shadow: 0 24px 36px -6px var(--element-glow); |
| } |
| .char-card-header { |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| } |
| .char-element-icon { |
| width: 60px; |
| height: 60px; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 30px; |
| border: 2px solid; |
| box-shadow: 0 0 25px var(--element-glow); |
| background: rgba(0,0,0,0.4); |
| backdrop-filter: blur(4px); |
| object-fit: cover; |
| } |
| .char-name { |
| font-size: 20px; |
| font-weight: 800; |
| margin-bottom: 4px; |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| } |
| .char-name a { |
| color: inherit; |
| text-decoration: none; |
| transition: 0.2s; |
| } |
| .char-name a:hover { |
| color: var(--accent); |
| text-decoration: underline; |
| } |
| .char-level { |
| color: var(--text-dim); |
| font-size: 12px; |
| background: rgba(0,0,0,0.4); |
| padding: 4px 10px; |
| border-radius: 30px; |
| display: inline-block; |
| border: 1px solid rgba(255,255,255,0.1); |
| } |
| .char-card-stats { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| background: rgba(0,0,0,0.3); |
| padding: 14px 18px; |
| border-radius: var(--radius-lg); |
| border: 1px solid rgba(255,255,255,0.05); |
| } |
| .weapon-badge { |
| background: rgba(255,255,255,0.08); |
| padding: 6px 16px; |
| border-radius: 40px; |
| font-size: 12px; |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| border: 1px solid rgba(255,255,255,0.1); |
| backdrop-filter: blur(4px); |
| } |
| .role-badge { |
| background: linear-gradient(135deg, rgba(139,92,246,0.25), rgba(167,139,250,0.25)); |
| padding: 4px 14px; |
| border-radius: 30px; |
| font-size: 12px; |
| font-weight: 700; |
| color: #fff; |
| border: 1px solid rgba(255,255,255,0.15); |
| backdrop-filter: blur(4px); |
| } |
| .char-card-sets { |
| font-size: 13px; |
| color: var(--text-soft); |
| background: rgba(0,0,0,0.2); |
| padding: 14px 18px; |
| border-radius: var(--radius-md); |
| line-height: 1.5; |
| border: 1px solid rgba(255,255,255,0.03); |
| } |
| |
| |
| .artifacts-grid { |
| display: flex; |
| flex-direction: column; |
| gap: 16px; |
| } |
| .artifact-card { |
| background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9)); |
| backdrop-filter: blur(8px); |
| border-radius: var(--radius-lg); |
| padding: 20px; |
| display: flex; |
| flex-wrap: wrap; |
| align-items: center; |
| gap: 20px; |
| border: 1px solid rgba(255,255,255,0.05); |
| box-shadow: 0 10px 24px -6px rgba(0,0,0,0.5); |
| transition: 0.2s; |
| } |
| .artifact-card:hover { |
| border-color: var(--accent); |
| transform: translateX(6px); |
| background: rgba(37,43,56,0.9); |
| } |
| .artifact-mainstat { |
| font-weight: 700; |
| color: #ffd966; |
| background: rgba(255, 217, 102, 0.12); |
| padding: 6px 18px; |
| border-radius: 30px; |
| border: 1px solid rgba(255, 217, 102, 0.3); |
| backdrop-filter: blur(4px); |
| font-size: 13px; |
| } |
| |
| |
| .ai-btn { |
| background: linear-gradient(135deg, #8b5cf6, #c084fc); |
| border: none; |
| color: white; |
| padding: 10px 20px; |
| border-radius: 30px; |
| font-size: 13px; |
| cursor: pointer; |
| font-weight: 700; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| white-space: nowrap; |
| transition: 0.25s; |
| box-shadow: 0 6px 16px rgba(139,92,246,0.4); |
| border: 1px solid rgba(255,255,255,0.2); |
| } |
| .ai-btn:hover { |
| transform: scale(1.02); |
| box-shadow: 0 10px 22px rgba(139,92,246,0.6); |
| } |
| .btn { |
| background: linear-gradient(135deg, var(--accent), #a78bfa); |
| color: #fff; |
| border: none; |
| padding: 10px 22px; |
| border-radius: 30px; |
| font-weight: 700; |
| cursor: pointer; |
| font-size: 13px; |
| transition: 0.2s; |
| box-shadow: 0 5px 14px var(--accent-glow); |
| border: 1px solid rgba(255,255,255,0.1); |
| } |
| .btn:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 20px var(--accent-glow); |
| } |
| .btn-outline { |
| background: transparent; |
| border: 1px solid rgba(255,255,255,0.15); |
| box-shadow: none; |
| } |
| .btn-outline:hover { |
| background: rgba(255,255,255,0.06); |
| border-color: var(--accent); |
| } |
| |
| |
| .cv-meter { |
| width: 100px; |
| height: 6px; |
| background: rgba(255,255,255,0.1); |
| border-radius: 3px; |
| overflow: hidden; |
| margin-top: 6px; |
| border: 1px solid rgba(255,255,255,0.05); |
| } |
| .cv-fill { |
| height: 100%; |
| transition: 0.5s; |
| border-radius: 3px; |
| } |
| |
| |
| .synergy-card { |
| background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9)); |
| backdrop-filter: blur(10px); |
| border-radius: var(--radius-xl); |
| padding: 28px; |
| border: 1px solid rgba(255,255,255,0.06); |
| margin-bottom: 24px; |
| position: relative; |
| overflow: hidden; |
| box-shadow: 0 18px 32px -8px rgba(0,0,0,0.6); |
| transition: 0.2s; |
| } |
| .synergy-card:hover { |
| border-color: rgba(255,255,255,0.15); |
| background: rgba(32,38,54,0.9); |
| } |
| .team-members { |
| display: flex; |
| gap: 20px; |
| flex-wrap: wrap; |
| margin: 20px 0; |
| } |
| .team-member { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| background: rgba(0,0,0,0.3); |
| padding: 16px 14px; |
| border-radius: var(--radius-lg); |
| min-width: 100px; |
| backdrop-filter: blur(8px); |
| border: 1px solid rgba(255,255,255,0.08); |
| transition: 0.2s; |
| } |
| .team-member:hover { |
| background: rgba(139,92,246,0.15); |
| border-color: var(--accent); |
| transform: translateY(-4px); |
| } |
| .member-icon { |
| width: 64px; |
| height: 64px; |
| border-radius: 50%; |
| background: rgba(0,0,0,0.5); |
| border: 2px solid; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 28px; |
| margin-bottom: 10px; |
| box-shadow: 0 0 25px var(--element-glow); |
| object-fit: cover; |
| } |
| |
| |
| .loader { |
| width: 22px; |
| height: 22px; |
| border: 3px solid rgba(255,255,255,0.3); |
| border-top-color: #fff; |
| border-radius: 50%; |
| animation: spin 0.8s linear infinite; |
| } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| |
| |
| .empty { |
| padding: 40px; |
| text-align: center; |
| color: var(--text-dim); |
| background: rgba(0,0,0,0.2); |
| border-radius: var(--radius-xl); |
| backdrop-filter: blur(8px); |
| border: 1px solid rgba(255,255,255,0.05); |
| } |
| |
| |
| .view-panel { display: none; } |
| .view-panel.active { display: block; } |
| .view-toggle { |
| display: flex; |
| gap: 10px; |
| margin-left: auto; |
| } |
| .view-toggle-btn { |
| background: rgba(0,0,0,0.3); |
| border: 1px solid rgba(255,255,255,0.08); |
| padding: 8px 18px; |
| border-radius: 30px; |
| font-size: 13px; |
| cursor: pointer; |
| color: var(--text-dim); |
| transition: 0.2s; |
| backdrop-filter: blur(4px); |
| } |
| .view-toggle-btn.active { |
| background: linear-gradient(135deg, var(--accent), #a78bfa); |
| color: #fff; |
| border-color: transparent; |
| box-shadow: 0 4px 10px var(--accent-glow); |
| } |
| |
| |
| .assessment-card { |
| background: linear-gradient(145deg, rgba(26,30,42,0.9), rgba(16,18,26,0.9)); |
| backdrop-filter: blur(10px); |
| border-radius: var(--radius-xl); |
| padding: 28px; |
| margin-bottom: 24px; |
| border-left: 6px solid; |
| border: 1px solid rgba(255,255,255,0.06); |
| box-shadow: 0 14px 28px -8px rgba(0,0,0,0.5); |
| } |
| |
| |
| #ai-overlay { |
| position: fixed; |
| right: -520px; |
| top: 0; |
| width: 520px; |
| height: 100vh; |
| background: rgba(15, 17, 23, 0.98); |
| backdrop-filter: blur(24px); |
| -webkit-backdrop-filter: blur(24px); |
| border-left: 1px solid rgba(255,255,255,0.1); |
| z-index: 2000; |
| transition: 0.3s cubic-bezier(0.2, 0.9, 0.4, 1); |
| display: flex; |
| flex-direction: column; |
| padding: 32px; |
| box-shadow: -14px 0 40px rgba(0,0,0,0.8); |
| } |
| #ai-overlay.open { right: 0; } |
| #ai-text { |
| overflow-y: auto; |
| padding-right: 8px; |
| height: calc(100vh - 110px); |
| } |
| .toc { |
| background: rgba(139,92,246,0.1); |
| border-left: 4px solid var(--accent); |
| padding: 14px 18px; |
| border-radius: var(--radius-md); |
| margin-bottom: 20px; |
| font-size: 13px; |
| } |
| .toc a { |
| color: var(--text-soft); |
| text-decoration: none; |
| margin-right: 14px; |
| display: inline-block; |
| padding: 4px 0; |
| } |
| .toc a:hover { |
| color: #fff; |
| text-decoration: underline; |
| } |
| |
| |
| .settings-section { |
| background: rgba(0,0,0,0.2); |
| border-radius: var(--radius-xl); |
| padding: 24px; |
| margin-bottom: 24px; |
| border: 1px solid rgba(255,255,255,0.05); |
| backdrop-filter: blur(8px); |
| } |
| .settings-row { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 18px; |
| padding-bottom: 18px; |
| border-bottom: 1px solid rgba(255,255,255,0.05); |
| } |
| .settings-row:last-child { border-bottom: none; } |
| .settings-label { |
| font-size: 14px; |
| font-weight: 600; |
| color: var(--text-soft); |
| } |
| .settings-description { |
| font-size: 12px; |
| color: var(--text-dim); |
| margin-top: 4px; |
| } |
| .provider-tabs { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 10px; |
| margin-bottom: 20px; |
| } |
| .provider-tab { |
| padding: 8px 16px; |
| background: rgba(255,255,255,0.05); |
| border-radius: 30px; |
| font-size: 13px; |
| font-weight: 600; |
| color: var(--text-dim); |
| cursor: pointer; |
| border: 1px solid rgba(255,255,255,0.08); |
| transition: 0.2s; |
| } |
| .provider-tab.active { |
| background: linear-gradient(135deg, var(--accent), #a78bfa); |
| color: #fff; |
| border-color: transparent; |
| } |
| |
| |
| .profile-badge { |
| background: rgba(139,92,246,0.2); |
| border: 1px solid rgba(139,92,246,0.3); |
| border-radius: 30px; |
| padding: 8px 18px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 13px; |
| } |
| |
| |
| .teams-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| gap: 20px; |
| margin-top: 20px; |
| } |
| .team-card { |
| background: rgba(26,30,42,0.7); |
| backdrop-filter: blur(8px); |
| border-radius: var(--radius-lg); |
| padding: 20px; |
| border: 1px solid rgba(255,255,255,0.05); |
| } |
| .team-card-header { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| margin-bottom: 14px; |
| } |
| .team-card-icons { |
| display: flex; |
| gap: 6px; |
| flex-wrap: wrap; |
| } |
| .team-card-icon { |
| width: 44px; |
| height: 44px; |
| border-radius: 50%; |
| background: rgba(0,0,0,0.3); |
| border: 2px solid; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 18px; |
| object-fit: cover; |
| } |
| |
| |
| .tooltip-icon { |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: rgba(255,255,255,0.1); |
| color: var(--text-dim); |
| font-size: 12px; |
| cursor: pointer; |
| margin-left: 6px; |
| border: 1px solid rgba(255,255,255,0.2); |
| transition: 0.2s; |
| } |
| .tooltip-icon:hover { |
| background: var(--accent); |
| color: #fff; |
| } |
| |
| |
| .copyright-notice { |
| margin-top: 30px; |
| padding-top: 20px; |
| border-top: 1px solid rgba(255,255,255,0.1); |
| font-size: 12px; |
| color: var(--text-dim); |
| text-align: center; |
| } |
| .copyright-notice a { |
| color: var(--accent); |
| text-decoration: none; |
| } |
| .copyright-notice a:hover { |
| text-decoration: underline; |
| } |
| |
| |
| .resonance-gradient { |
| background: linear-gradient(90deg, var(--hydro), var(--pyro), var(--electro)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| font-weight: bold; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <div id="legendPopup" class="blur-popup"> |
| <div class="popup-content"> |
| <span class="popup-close" onclick="closeLegend()">×</span> |
| <h2 style="font-size: 28px; margin-bottom: 20px; background: linear-gradient(135deg, #fff, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">📘 Легенда</h2> |
| <h3 style="margin-bottom: 16px; font-size: 18px;">🎨 Цвета элементов</h3> |
| <div class="legend-grid" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;"> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--pyro); border-radius: 4px;"></span> Пиро 🔥</div> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--hydro); border-radius: 4px;"></span> Гидро 💧</div> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--anemo); border-radius: 4px;"></span> Анемо 🍃</div> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--electro); border-radius: 4px;"></span> Электро ⚡</div> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--dendro); border-radius: 4px;"></span> Дендро 🌿</div> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--cryo); border-radius: 4px;"></span> Крио ❄️</div> |
| <div style="display: flex; align-items: center; gap: 10px;"><span style="width: 16px; height: 16px; background: var(--geo); border-radius: 4px;"></span> Гео 🪨</div> |
| </div> |
| <h3 style="margin: 28px 0 16px; font-size: 18px;">📏 Обозначения статов</h3> |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px;"> |
| <div><strong>Сила атаки</strong> — ATK</div> |
| <div><strong>Сила атаки%</strong> — ATK%</div> |
| <div><strong>Защита</strong> — DEF</div> |
| <div><strong>Защита%</strong> — DEF%</div> |
| <div><strong>HP</strong> — HP</div> |
| <div><strong>HP%</strong> — HP%</div> |
| <div><strong>Мастерство стихий</strong> — EM</div> |
| <div><strong>Восстановление энергии</strong> — ER</div> |
| <div><strong>Крит. шанс%</strong> — CR</div> |
| <div><strong>Крит. урон%</strong> — CD</div> |
| </div> |
| <button class="btn" style="margin-top: 28px; width: 100%;" onclick="closeLegend()">Закрыть</button> |
| </div> |
| </div> |
|
|
| |
| <div id="cvPopup" class="blur-popup"> |
| <div class="popup-content"> |
| <span class="popup-close" onclick="closeCVPopup()">×</span> |
| <h2 style="font-size: 28px; margin-bottom: 20px; color: var(--accent);">⚔️ Крит-ценность (CV)</h2> |
| <p style="font-size: 16px; line-height: 1.6; margin-bottom: 20px; color: var(--text-soft);"> |
| <strong>CV = CRIT Rate × 2 + CRIT DMG</strong> |
| </p> |
| <p style="margin-bottom: 20px; color: var(--text-dim);">Метрика для оценки качества артефактов. Чем выше, тем лучше.</p> |
| <div style="background: rgba(0,0,0,0.3); padding: 20px; border-radius: var(--radius-lg);"> |
| <div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;"> |
| <div style="width: 20px; height: 20px; background: var(--success); border-radius: 6px;"></div> |
| <span><strong>Высокий (>200)</strong> — отличный билд</span> |
| </div> |
| <div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;"> |
| <div style="width: 20px; height: 20px; background: var(--warning); border-radius: 6px;"></div> |
| <span><strong>Средний (140–200)</strong> — хороший билд</span> |
| </div> |
| <div style="display: flex; align-items: center; gap: 16px;"> |
| <div style="width: 20px; height: 20px; background: var(--error); border-radius: 6px;"></div> |
| <span><strong>Низкий (<140)</strong> — есть куда расти</span> |
| </div> |
| </div> |
| <button class="btn" style="margin-top: 28px; width: 100%;" onclick="closeCVPopup()">Закрыть</button> |
| </div> |
| </div> |
|
|
| |
| <aside> |
| <div class="side-header"> |
| <h1>NEXUS BETA</h1> |
| <p>анализатор Genshin (Разработка)</p> |
| </div> |
| <div class="nav-group"> |
| <div class="nav-item active" data-tab="chars" onclick="switchTab('chars')">👥 <span>Персонажи</span></div> |
| <div class="nav-item" data-tab="artifacts" onclick="switchTab('artifacts')">💎 <span>Артефакты</span></div> |
| <div class="nav-item" data-tab="team" onclick="switchTab('team')">⚔️ <span>Отряды</span></div> |
| <div class="nav-item" data-tab="assessment" onclick="switchTab('assessment')">📊 <span>Оценка</span></div> |
| <div class="nav-item" data-tab="myteams" onclick="switchTab('myteams')">📁 <span>Мои отряды</span></div> |
| <div class="nav-item" data-tab="profiles" onclick="switchTab('profiles')">👤 <span>Профили</span></div> |
| <div class="nav-item" data-tab="settings" onclick="switchTab('settings')">⚙️ <span>Настройки</span></div> |
| </div> |
| <div class="filter-section"> |
| <label>Фильтр по стихии</label> |
| <select id="f-el" onchange="applyFilters()"> |
| <option value="all">Все элементы</option> |
| <option value="pyro">Пиро</option> |
| <option value="hydro">Гидро</option> |
| <option value="anemo">Анемо</option> |
| <option value="electro">Электро</option> |
| <option value="dendro">Дендро</option> |
| <option value="cryo">Крио</option> |
| <option value="geo">Гео</option> |
| </select> |
| <label>Сортировка</label> |
| <select id="f-sort" onchange="applyFilters()"> |
| <option value="cv">Крит‑ценность (CV)</option> |
| <option value="lvl">Уровень</option> |
| <option value="name">Имя</option> |
| </select> |
| <button class="btn" style="width:100%; background: linear-gradient(135deg, #ef4444, #b91c1c);" onclick="clearDB()">🗑️ <span>Удалить данные</span></button> |
| <button class="btn" style="width:100%; margin-top: 14px; background: linear-gradient(135deg, #4b5563, #1f2937);" onclick="openLegend()">📖 <span>Легенда</span></button> |
| </div> |
| </aside> |
|
|
| |
| <main> |
| <header> |
| <div class="stats-strip"> |
| <div class="stat-unit"><small>Персонажи</small><b id="total-c">0</b></div> |
| <div class="stat-unit"><small>Артефакты</small><b id="total-a">0</b></div> |
| <div class="stat-unit"><small>Средний CV</small><b id="avg-cv">0</b></div> |
| </div> |
| <div style="display: flex; gap: 12px;"> |
| <label class="btn"> |
| 📂 <span>Загрузить GOOD</span> <div class="settings-description">ПО для Сбора базы > <a href="https://github.com/konkers/irminsul" target="_blank">irminsul(GitHub)</a></div> |
| <input type="file" id="fileIn" accept=".json" style="display:none;"> |
| </label> |
| <button class="btn btn-outline" onclick="showProfileSelector()" style="display:flex; align-items:center; gap:6px;"> |
| 👤 <span>Профили</span> |
| </button> |
| </div> |
| </header> |
| |
| <div class="content-area"> |
| |
| <div id="view-chars" class="view-panel active"> |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:16px;"> |
| <h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">👥 Персонажи</h2> |
| <div class="view-toggle"> |
| <span class="view-toggle-btn active" data-view="table" onclick="setCharsView('table')">📋 Таблица</span> |
| <span class="view-toggle-btn" data-view="cards" onclick="setCharsView('cards')">🃏 Карточки</span> |
| </div> |
| </div> |
| <div id="chars-table-container" style="display:block; overflow-x: auto;"> |
| <table class="data-table"> |
| <thead> |
| <tr> |
| <th>Персонаж</th> |
| <th>Оружие / Роль</th> |
| <th>Экипированные сеты</th> |
| <th>Крит‑ценность (CV) <span class="tooltip-icon" onclick="openCVPopup()">?</span></th> |
| <th>✨ AI‑анализ</th> |
| </tr> |
| </thead> |
| <tbody id="char-body"></tbody> |
| </table> |
| </div> |
| <div id="chars-cards-container" style="display:none;" class="chars-grid"></div> |
| <div id="empty-msg" class="empty" style="display:none;"> |
| 📁 Нет данных. Загрузите GOOD‑файл. |
| </div> |
| </div> |
| |
| |
| <div id="view-artifacts" class="view-panel"> |
| <div style="display:flex; gap:16px; margin-bottom:24px; align-items:center; flex-wrap:wrap;"> |
| <h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, #ffd966); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">💎 Артефакты</h2> |
| <select id="art-set-filter" onchange="renderArtifacts()" style="width:200px;"> |
| <option value="all">Все сеты</option> |
| </select> |
| <select id="art-set-group" onchange="groupArtifacts()" style="width:180px;"> |
| <option value="all">Все группы</option> |
| <option value="set">По сетам</option> |
| <option value="slot">По слотам</option> |
| <option value="rarity">По редкости</option> |
| <option value="level">По уровню</option> |
| <option value="cv">По CV</option> |
| </select> |
| <select id="art-slot-filter" onchange="renderArtifacts()" style="width:180px;"> |
| <option value="all">Все слоты</option> |
| <option value="flower">Цветок жизни</option> |
| <option value="plume">Перо смерти</option> |
| <option value="sands">Пески времени</option> |
| <option value="goblet">Кубок пространства</option> |
| <option value="circlet">Корона разума</option> |
| </select> |
| <div class="view-toggle" style="margin-left:auto;"> |
| <span class="view-toggle-btn active" data-view="list" onclick="setArtifactsView('list')">📋 Список</span> |
| <span class="view-toggle-btn" data-view="grid" onclick="setArtifactsView('grid')">🃏 Карточки</span> |
| </div> |
| </div> |
| <div id="artifacts-list" class="artifacts-grid"></div> |
| </div> |
| |
| |
| <div id="view-team" class="view-panel"> |
| <div style="display:flex; align-items:center; gap:20px; margin-bottom:24px; flex-wrap:wrap;"> |
| <h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, #fbbf24); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">⚔️ Отряды</h2> |
| <button class="btn" onclick="aiTeamBuilder()" style="background: linear-gradient(135deg, #8b5cf6, #c084fc); padding: 12px 24px;"> |
| 🧠 AI‑подбор |
| </button> |
| <label style="display:flex; align-items:center; gap:8px; background:rgba(0,0,0,0.3); padding:8px 20px; border-radius:40px; border:1px solid rgba(255,255,255,0.1);"> |
| <input type="checkbox" id="googleSearchCheckbox" style="width:18px; height:18px; accent-color:var(--accent);"> |
| 🔍 Google поиск (Gemini) |
| </label> |
| </div> |
| <div id="team-suggestions" style="display:flex; flex-direction:column; gap:20px;"></div> |
| <div id="ai-team-result" style="margin-top:24px;"></div> |
| </div> |
| |
| |
| <div id="view-assessment" class="view-panel"> |
| <h2 style="font-size:26px; font-weight:800; margin-bottom:24px; background: linear-gradient(135deg, #fff, #10b981); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">📊 Оценка аккаунта</h2> |
| <div id="assessment-content">Загрузите GOOD‑файл для точной оценки.</div> |
| </div> |
| |
| |
| <div id="view-myteams" class="view-panel"> |
| <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:16px;"> |
| <h2 style="font-size:26px; font-weight:800; background: linear-gradient(135deg, #fff, #f59e0b); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">📁 Мои отряды</h2> |
| <div style="display:flex; gap:12px;"> |
| <button class="btn" onclick="generateRandomTeam()" style="background: linear-gradient(135deg, #10b981, #059669);"> |
| 🎲 Рандомный |
| </button> |
| <button class="btn" onclick="saveCurrentTeam()" style="background: linear-gradient(135deg, #8b5cf6, #a78bfa);"> |
| 💾 Сохранить |
| </button> |
| </div> |
| </div> |
| <div id="myteams-content"> |
| <div id="myteams-grid" class="teams-grid"></div> |
| </div> |
| </div> |
| |
| |
| <div id="view-profiles" class="view-panel"> |
| <h2 style="font-size:26px; font-weight:800; margin-bottom:24px; background: linear-gradient(135deg, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">👤 Профили GOOD</h2> |
| <div id="profiles-content"> |
| <div style="display:flex; gap:16px; margin-bottom:24px; flex-wrap:wrap;"> |
| <button class="btn" onclick="createNewProfile()" style="background: linear-gradient(135deg, #8b5cf6, #a78bfa);"> |
| ➕ Новый профиль |
| </button> |
| <button class="btn btn-outline" onclick="loadProfile(activeProfile)" style="display:flex; align-items:center; gap:6px;"> |
| 🔄 Загрузить текущий |
| </button> |
| </div> |
| <div id="profiles-list" style="display:flex; flex-direction:column; gap:14px;"></div> |
| </div> |
| </div> |
| |
| |
| <div id="view-settings" class="view-panel"> |
| <h2 style="font-size:26px; font-weight:800; margin-bottom:24px; background: linear-gradient(135deg, #fff, #94a3b8); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">⚙️ Настройки</h2> |
| <div class="settings-section"> |
| <h3 style="font-size:20px; margin-bottom:20px;">🤖 AI‑провайдер</h3> |
| <div class="provider-tabs" id="providerTabs"> |
| <div class="provider-tab active" data-provider="huggingface" onclick="setAIProvider('huggingface')">🤗 Hugging Face</div> |
| <div class="provider-tab" data-provider="openai" onclick="setAIProvider('openai')">⚡ OpenAI</div> |
| <div class="provider-tab" data-provider="gemini" onclick="setAIProvider('gemini')">🌀 Gemini</div> |
| <div class="provider-tab" data-provider="deepseek" onclick="setAIProvider('deepseek')">🎯 DeepSeek</div> |
| <div class="provider-tab" data-provider="custom" onclick="setAIProvider('custom')">🔧 Custom</div> |
| </div> |
| <div id="provider-settings"> |
| |
| <div id="settings-huggingface" style="display:block;"> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🔑 Hugging Face токен</div> |
| <div class="settings-description">Получите на <a href="https://huggingface.co/settings/tokens/new?ownUserPermissions=inference.serverless.write&tokenType=fineGrained" target="_blank">huggingface.co/settings/tokens</a></div> |
| </div> |
| <input type="password" id="hf-api-key" placeholder="hf_..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🧠 Модель Hugging Face</div> |
| <div class="settings-description">Выберите модель для инференса</div> |
| </div> |
| <select id="hf-model" style="width:240px;"> |
| <option value="Qwen/Qwen3-32B">Qwen/Qwen3-32B</option> |
| <option value="Qwen/Qwen3-4B-Instruct-2507">Qwen3-4B-Instruct</option> |
| <option value="deepseek-ai/DeepSeek-R1:fastest" selected>DeepSeek-R1:fastest</option> |
| <option value="deepseek-ai/DeepSeek-R1-0528">DeepSeek-R1-0528</option> |
| <option value="zai-org/GLM-4.7-Flash">GLM-4.7-Flash</option> |
| </select> |
| </div> |
| </div> |
| |
| |
| <div id="settings-openai" style="display:none;"> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🔑 OpenAI API ключ</div> |
| <div class="settings-description">sk-... с platform.openai.com</div> |
| </div> |
| <input type="password" id="openai-api-key" placeholder="sk-..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🧠 Модель OpenAI</div> |
| </div> |
| <select id="openai-model" style="width:240px;"> |
| <option value="gpt-5-mini-2025-08-07">gpt-5-mini-2025-08-07</option> |
| <option value="gpt-5-nano-2025-08-07">gpt-5-nano-2025-08-07</option> |
| <option value="gpt-oss-20b">gpt-oss-20b</option> |
| </select> |
| </div> |
| </div> |
| |
| |
| <div id="settings-gemini" style="display:none;"> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🔑 Gemini API ключ</div> |
| <div class="settings-description">AIza... с <a href="https://aistudio.google.com/u/0/api-keys" target="_blank">aistudio.google.com</a></div> |
| </div> |
| <input type="password" id="gemini-api-key" placeholder="AIza..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🧠 Модель Gemini</div> |
| </div> |
| <select id="gemini-model" style="width:240px;"> |
| <option value="gemini-2.5-pro">Gemini 2.5 Pro</option> |
| <option value="gemini-2.5-flash-lite-preview-09-2025">Gemini 2.0 Flash</option> |
| <option value="gemini-3-flash-preview:search">Gemini 3 Flash (search)</option> |
| </select> |
| </div> |
| </div> |
| |
| |
| <div id="settings-deepseek" style="display:none;"> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🔑 DeepSeek API ключ</div> |
| <div class="settings-description">Получите на <a href="https://platform.deepseek.com/api_keys" target="_blank">platform.deepseek.com/api_keys</a></div> |
| </div> |
| <input type="password" id="deepseek-api-key" placeholder="sk-..." value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🧠 Модель DeepSeek</div> |
| </div> |
| <select id="deepseek-model" style="width:240px;"> |
| <option value="deepseek-chat">DeepSeek Chat</option> |
| <option value="deepseek-coder">DeepSeek Coder</option> |
| <option value="deepseek-reasoner">DeepSeek Reasoner</option> |
| </select> |
| </div> |
| </div> |
| |
| |
| <div id="settings-custom" style="display:none;"> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🔑 Custom API ключ</div> |
| </div> |
| <input type="password" id="custom-api-key" placeholder="ключ" value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🌐 Custom Base URL</div> |
| </div> |
| <input type="text" id="custom-base-url" placeholder="https://..." value="https://router.huggingface.co/v1" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🧠 Custom Model</div> |
| </div> |
| <input type="text" id="custom-model" placeholder="model-name" value="" style="width:240px; background:rgba(0,0,0,0.3); border:1px solid rgba(255,255,255,0.1); padding:10px 18px; border-radius:30px; color:#fff;"> |
| </div> |
| </div> |
| </div> |
| <div style="display:flex; gap:16px; margin-top:20px;"> |
| <button class="btn" onclick="saveAllApiKeys()">💾 Сохранить все ключи</button> |
| <button class="btn btn-outline" onclick="testCurrentApi()">🔍 Проверить текущий</button> |
| </div> |
| <div id="api-test-result" style="margin-top:16px; font-size:13px;"></div> |
| </div> |
| |
| <div class="settings-section"> |
| <h3 style="font-size:20px; margin-bottom:20px;">🎨 Интерфейс</h3> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🌓 Тема оформления</div> |
| <div class="settings-description">Стиль отображения</div> |
| </div> |
| <select id="theme-selector" onchange="changeTheme(this.value)" style="width:160px;"> |
| <option value="dark">Тёмная</option> |
| <option value="darker">Угольно-чёрная</option> |
| <option value="light">Светлая</option> |
| </select> |
| </div> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🗣️ Язык интерфейса</div> |
| <div class="settings-description">Только русский</div> |
| </div> |
| <select disabled style="width:160px; opacity:0.7;"> |
| <option selected>Русский</option> |
| </select> |
| </div> |
| </div> |
| |
| <div class="settings-section"> |
| <h3 style="font-size:20px; margin-bottom:20px;">💾 Данные</h3> |
| <div class="settings-row"> |
| <div> |
| <div class="settings-label">🗄️ Управление БД</div> |
| <div class="settings-description">Сброс или экспорт</div> |
| </div> |
| <div style="display:flex; gap:12px;"> |
| <button class="btn" style="background:linear-gradient(135deg, #ef4444, #b91c1c);" onclick="clearDB()">♻️ Сбросить</button> |
| <button class="btn" onclick="exportGOOD()">📤 Экспорт</button> |
| </div> |
| </div> |
| |
| <div class="copyright-notice"> |
| <p>© 2026 Genshin Nexus - фанатский проект. Не связан с miHoYo/Hoyoverse.</p> |
| <p>Разработано <a href="https://github.com/assBuk" target="_blank">Mycc</a></p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| |
| <div id="ai-overlay"> |
| <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;"> |
| <h3 style="font-size:24px; background: linear-gradient(135deg, #fff, var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">✨ AI‑анализ</h3> |
| <button style="background:none; border:none; color:#fff; font-size:32px; cursor:pointer;" onclick="closeAI()">×</button> |
| </div> |
| <div id="ai-toc" class="toc" style="display: none;"></div> |
| <div id="ai-text" style="font-size:15px; line-height:1.6; color:var(--text-soft); overflow-y: auto; padding-right: 6px;"> |
| 💡 Нажмите «✨ Анализ» у персонажа. |
| </div> |
| <div id="ai-loader" class="loader" style="margin: 40px auto; display: none;"></div> |
| </div> |
|
|
| <script> |
| |
| const artifactSetNames = { |
| 'GladiatorsFinale': 'Конец гладиатора', |
| 'WanderersTroupe': 'Странствующий ансамбль', |
| 'NoblesseOblige': 'Церемония древней знати', |
| 'ArchaicPetra': 'Архаичный камень', |
| 'Thundersoother': 'Усмиряющий гром', |
| 'ThunderingFury': 'Громогласный рёв ярости', |
| 'EmblemOfSeveredFate': 'Эмблема рассечённой судьбы', |
| 'PaleFlame': 'Бледный огонь', |
| 'BloodstainedChivalry': 'Рыцарство крови', |
| 'ShimenawasReminiscence': 'Воспоминания Симэнавы', |
| 'HeartOfDepth': 'Сердце глубин', |
| 'ViridescentVenerer': 'Изумрудная тень', |
| 'HuskOfOpulentDreams': 'Кокон сладких грёз', |
| 'OceanHuedClam': 'Моллюск морских красок', |
| 'VermillionHereafter': 'Киноварное загробье', |
| 'EchoesOfAnOffering': 'Отголоски подношения', |
| 'DeepwoodMemories': 'Воспоминания дремучего леса', |
| 'GildedDreams': 'Позолоченные сны', |
| 'DesertPavilionChronicle': 'Хроники Чертогов в пустыне', |
| 'FlowerOfParadiseLost': 'Цветок потерянного рая', |
| 'VourukashasGlow': 'Сияние Вурукаши', |
| 'MarechausseeHunter': 'Охотник Сумеречного двора', |
| 'GoldenTroupe': 'Золотая труппа', |
| 'ObsidianCodex': 'Кодекс Обсидиана', |
| 'ScrollOfTheHeroOfCinderCity': 'Свиток героя Пепельного города', |
| 'SongOfDaysPast': 'Песнь былых дней', |
| 'NighttimeWhispersInTheEchoingWoods': 'Ночной шёпот в Эхо-лесу', |
| 'FragmentOfHarmonicWhimsy': 'Фрагмент гармонической прихоти', |
| 'UnfinishedReverie': 'Неоконченные грёзы', |
| 'FinaleOfTheDeepGalleries': 'Финал глубоких галерей', |
| 'LongNightsOath': 'Клятва долгой ночи', |
| 'NightOfTheSkysUnveiling': 'Ночь раскрытого неба', |
| 'SilkenMoonsSerenade': 'Шёлковая серенада луны', |
| 'ADayCarvedFromRisingWinds': 'День, высеченный из восходящих ветров', |
| 'AubadeOfMorningstarAndMoon': 'Утренняя серенада звезды и луны', |
| 'TenacityOfTheMillelith': 'Стойкость Миллелита', |
| 'CrimsonWitchOfFlames': 'Горящая алая ведьма', |
| 'Lavawalker': 'Ступающий по лаве', |
| 'BlizzardStrayer': 'Заблудший в метели', |
| 'MaidenBeloved': 'Возлюбленная юная дева' |
| }; |
| |
| const slotNames = { |
| 'flower': '🌺 Цветок жизни', |
| 'plume': '🪶 Перо смерти', |
| 'sands': '⏳ Пески времени', |
| 'goblet': '🏺 Кубок пространства', |
| 'circlet': '👑 Корона разума' |
| }; |
| |
| const statNames = { |
| 'hp': 'HP', |
| 'hp_': 'HP%', |
| 'atk': '⚔️ Сила атаки', |
| 'atk_': '⚔️ Сила атаки%', |
| 'def': '🛡️ Защита', |
| 'def_': '🛡️ Защита%', |
| 'eleMas': '✨ Мастерство стихий', |
| 'enerRech_': '🔋 Восстановление энергии', |
| 'critRate_': '🎯 Крит. шанс%', |
| 'critDMG_': '💥 Крит. урон%', |
| 'heal_': '💚 Бонус лечения', |
| 'physical_dmg_': '🏹 Физ. урон%', |
| 'pyro_dmg_': '🔥 Пиро урон%', |
| 'hydro_dmg_': '💧 Гидро урон%', |
| 'anemo_dmg_': '🍃 Анемо урон%', |
| 'electro_dmg_': '⚡ Электро урон%', |
| 'dendro_dmg_': '🌿 Дендро урон%', |
| 'cryo_dmg_': '❄️ Крио урон%', |
| 'geo_dmg_': '🪨 Гео урон%' |
| }; |
| |
| |
| 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" } |
| ]; |
| |
| |
| const charElements = {}; |
| const roles = {}; |
| const elementIcons = { |
| pyro: '🔥', hydro: '💧', anemo: '🍃', electro: '⚡', |
| dendro: '🌿', cryo: '❄️', geo: '🪨' |
| }; |
| const roleNames = { |
| dps: 'Основной урон', subdps: 'Под-урон', support: 'Поддержка', |
| healer: 'Лекарь', shielder: 'Щитовик', battery: 'Батарея' |
| }; |
| |
| |
| const nameMapping = {}; |
| LOCAL_CHARACTERS.forEach(c => { |
| nameMapping[c.name] = c.name; |
| nameMapping[c.name.replace(/ /g, '')] = c.name; |
| }); |
| nameMapping['Arlecchino'] = 'Арлекино'; |
| nameMapping['Furina'] = 'Фурина'; |
| nameMapping['Neuvillette'] = 'Нёвиллет'; |
| nameMapping['Kazuha'] = 'Кадзуха'; |
| nameMapping['Zhongli'] = 'Чжун Ли'; |
| nameMapping['Bennett'] = 'Беннет'; |
| nameMapping['Chevreuse'] = 'Шеврёз'; |
| nameMapping['Fischl'] = 'Фишль'; |
| nameMapping['Nahida'] = 'Нахида'; |
| nameMapping['KukiShinobu'] = 'Шинобу'; |
| nameMapping['Wriothesley'] = 'Ризли'; |
| nameMapping['Shenhe'] = 'Шэнь Хэ'; |
| nameMapping['Kokomi'] = 'Кокоми'; |
| nameMapping['Navia'] = 'Навия'; |
| nameMapping['Xiangling'] = 'Сян Линь'; |
| nameMapping['Clorinde'] = 'Клоринда'; |
| nameMapping['RaidenShogun'] = 'Райдэн'; |
| nameMapping['Xingqiu'] = 'Син Цю'; |
| nameMapping['Yelan'] = 'Е Лань'; |
| nameMapping['Chiori'] = 'Тиори'; |
| nameMapping['Xilonen'] = 'Шилонен'; |
| nameMapping['Ororon'] = 'Оророн'; |
| nameMapping['Charlotte'] = 'Шарлотта'; |
| nameMapping['Gaming'] = 'Га Мин'; |
| nameMapping['Lynette'] = 'Линнет'; |
| nameMapping['LanYan'] = 'Лань Янь'; |
| nameMapping['Dehya'] = 'Дэхья'; |
| nameMapping['Thoma'] = 'Тома'; |
| nameMapping['Yanfei'] = 'Янь Фэй'; |
| nameMapping['Sethos'] = 'Сетос'; |
| nameMapping['Ifa'] = 'Ифу'; |
| nameMapping['Dahlia'] = 'Далия'; |
| nameMapping['Iansan'] = 'Иансан'; |
| nameMapping['YumemizukiMizuki'] = 'Мидзуки'; |
| nameMapping['Aino'] = 'Айно'; |
| nameMapping['Jahoda'] = 'Ягода'; |
| nameMapping['Columbina'] = 'Коломбина'; |
| nameMapping['Illuga'] = 'Иллуги'; |
| nameMapping['Traveler'] = 'ГГ (Анемо)'; |
| nameMapping['Skirk'] = 'Скирк'; |
| nameMapping['Kinich'] = 'Кинич'; |
| nameMapping['Emilie'] = 'Эмилия'; |
| nameMapping['Sigewinne'] = 'Сиджвин'; |
| nameMapping['Mualani'] = 'Муалани'; |
| nameMapping['Chasca'] = 'Часка'; |
| nameMapping['Citlali'] = 'Ситлали'; |
| nameMapping['Mavuika'] = 'Мавуика'; |
| nameMapping['Varesa'] = 'Вареса'; |
| |
| const elementMap = { |
| RaidenShogun: 'electro', Bennett: 'pyro', Nahida: 'dendro', Yelan: 'hydro', |
| Zhongli: 'geo', Arlecchino: 'pyro', Xiao: 'anemo', Ganyu: 'cryo', |
| Jean: 'anemo', Diluc: 'pyro', Mona: 'hydro', Fischl: 'electro', |
| Xiangling: 'pyro', Xingqiu: 'hydro', Sucrose: 'anemo', Noelle: 'geo', |
| KujouSara: 'electro', KukiShinobu: 'electro', Yaoyao: 'dendro', |
| Kirara: 'dendro', Collei: 'dendro', Alhaitham: 'dendro', Faruzan: 'anemo', |
| Chongyun: 'cryo', Diona: 'cryo', Rosaria: 'cryo', Kaeya: 'cryo', |
| Lisa: 'electro', Amber: 'pyro', Barbara: 'hydro', Sayu: 'anemo', |
| YunJin: 'geo', Ningguang: 'geo', Gorou: 'geo', Kaveh: 'dendro', |
| Mika: 'cryo', Freminet: 'cryo', Qiqi: 'cryo', Aloy: 'cryo', |
| Citlali: 'cryo', Chiori: 'geo', Xilonen: 'geo', Ororon: 'electro', |
| Charlotte: 'cryo', Gaming: 'pyro', Lynette: 'anemo', LanYan: 'anemo', |
| Chevreuse: 'pyro', Dehya: 'pyro', Thoma: 'pyro', Yanfei: 'pyro', |
| Sethos: 'electro', Ifa: 'anemo', Dahlia: 'hydro', Iansan: 'electro', |
| YumemizukiMizuki: 'anemo', Aino: 'hydro', Manekin: 'anemo', Manekina: 'anemo', |
| Jahoda: 'anemo', Columbina: 'hydro', Illuga: 'geo', Traveler: 'anemo', |
| Skirk: 'cryo', Furina: 'hydro', Navia: 'geo', Wriothesley: 'cryo', |
| Clorinde: 'electro', Sigewinne: 'hydro', Kinich: 'dendro', Emilie: 'dendro', |
| Neuvillette: 'hydro', Kazuha: 'anemo', Kokomi: 'hydro', Shenhe: 'cryo', |
| Mualani: 'hydro', Chasca: 'anemo', Mavuika: 'pyro', Varesa: 'electro' |
| }; |
| |
| |
| const metaTeams = [ |
| { |
| name: '🌊 Пузырьковый реактор', |
| fullName: 'Фурина + Нёвиллет', |
| chars: ['Furina','Neuvillette','Kazuha','Zhongli'], |
| resonance: 'Гидро резонанс: +25% HP', |
| buffs: 'Фурина: +~75% урон, Нёвиллет: ДД, Кадзуха: +40% элемента, Чжун Ли: -20% резист', |
| reactions: 'Пар + Заряжен', |
| meta: '6.3+', |
| description: 'Топ-1 мета-отряд. Нёвиллет наносит колоссальный урон заряженными атаками.' |
| }, |
| { |
| name: '🔥 Перегрузка Арлекино', |
| fullName: 'Арлекино + Шеврёз', |
| chars: ['Arlecchino','Bennett','Chevreuse','Fischl'], |
| resonance: 'Пиро резонанс: +25% ATK', |
| buffs: 'Арлекино: мощный ДД, Беннет: +~1000 ATK, Шеврёз: -40% резист', |
| reactions: 'Перегрузка', |
| meta: '6.3+', |
| description: 'Арлекино с Шеврёз дают огромный урон по площади.' |
| }, |
| { |
| name: '🌱 Гиперблум', |
| fullName: 'Нахида + Фурина + Куки', |
| chars: ['Nahida','Furina','KukiShinobu','Zhongli'], |
| resonance: 'Дендро +80 EM, Гидро +25% HP', |
| buffs: 'Нахида: +250 EM, Фурина: бафф, Куки: триггер', |
| reactions: 'Гиперблум', |
| meta: '6.3+', |
| description: 'Стабильный урон через ядра. Куки на EM наносит 30-35к за ядро.' |
| }, |
| { |
| name: '❄️ Моно-Крио', |
| fullName: 'Ризли + Шэнь Хэ', |
| chars: ['Wriothesley','Shenhe','Kazuha','Kokomi'], |
| resonance: 'Крио резонанс: +15% CR', |
| buffs: 'Ризли: ДД, Шэнь Хэ: бафф крио', |
| reactions: 'Заморозка', |
| meta: '6.3+', |
| description: 'Ризли бьёт быстро и сильно, Шэнь Хэ увеличивает крио урон.' |
| }, |
| { |
| name: '🪨 Гео-удар', |
| fullName: 'Навия + Чжун Ли', |
| chars: ['Navia','Zhongli','Bennett','Xiangling'], |
| resonance: 'Гео +15% урон, Пиро +25% ATK', |
| buffs: 'Навия: ДД, Чжун Ли: щит, Беннет: ATK', |
| reactions: 'Кристалл', |
| meta: '6.3+', |
| description: 'Навия наносит огромный урон дробовиком.' |
| }, |
| { |
| name: '⚡ Стимуляция', |
| fullName: 'Клоринда + Нахида', |
| chars: ['Clorinde','Nahida','Fischl','Zhongli'], |
| resonance: 'Электро резонанс', |
| buffs: 'Клоринда: ДД, Нахида: EM', |
| reactions: 'Стимуляция', |
| meta: '6.3+', |
| description: 'Клоринда — тир-1 ДД через обострение.' |
| } |
| ]; |
| |
| |
| let localData = null; |
| let dbInstance = null; |
| |
| |
| let currentProvider = localStorage.getItem('nexus_current_provider') || 'huggingface'; |
| let hfApiKey = localStorage.getItem('nexus_hf_key') || ''; |
| let openaiApiKey = localStorage.getItem('nexus_openai_key') || ''; |
| let geminiApiKey = localStorage.getItem('nexus_gemini_key') || ''; |
| let deepseekApiKey = localStorage.getItem('nexus_deepseek_key') || ''; |
| let customApiKey = localStorage.getItem('nexus_custom_key') || ''; |
| let customBaseUrl = localStorage.getItem('nexus_custom_url') || 'https://router.huggingface.co/v1'; |
| let customModel = localStorage.getItem('nexus_custom_model') || ''; |
| let hfModel = localStorage.getItem('nexus_hf_model') || 'deepseek-ai/DeepSeek-R1:fastest'; |
| let openaiModel = localStorage.getItem('nexus_openai_model') || 'gpt-oss-20b'; |
| let geminiModel = localStorage.getItem('nexus_gemini_model') || 'gemini-2.5-flash-lite-preview-09-2025'; |
| let deepseekModel = localStorage.getItem('nexus_deepseek_model') || 'deepseek-chat'; |
| let charsView = localStorage.getItem('nexus_chars_view') || 'table'; |
| let artifactsView = localStorage.getItem('nexus_artifacts_view') || 'list'; |
| |
| |
| let profiles = JSON.parse(localStorage.getItem('nexus_profiles') || '[]'); |
| let activeProfile = localStorage.getItem('nexus_active_profile') || 'default'; |
| |
| |
| let myTeams = JSON.parse(localStorage.getItem('nexus_myteams') || '[]'); |
| |
| |
| function openLegend() { document.getElementById('legendPopup').classList.add('active'); } |
| function closeLegend() { document.getElementById('legendPopup').classList.remove('active'); } |
| function openCVPopup() { document.getElementById('cvPopup').classList.add('active'); } |
| function closeCVPopup() { document.getElementById('cvPopup').classList.remove('active'); } |
| |
| |
| function setAIProvider(provider) { |
| currentProvider = provider; |
| localStorage.setItem('nexus_current_provider', provider); |
| |
| document.querySelectorAll('.provider-tab').forEach(tab => { |
| tab.classList.toggle('active', tab.dataset.provider === provider); |
| }); |
| |
| document.querySelectorAll('#provider-settings > div').forEach(div => div.style.display = 'none'); |
| document.getElementById(`settings-${provider}`).style.display = 'block'; |
| } |
| |
| |
| function saveAllApiKeys() { |
| hfApiKey = document.getElementById('hf-api-key').value.trim(); |
| localStorage.setItem('nexus_hf_key', hfApiKey); |
| openaiApiKey = document.getElementById('openai-api-key').value.trim(); |
| localStorage.setItem('nexus_openai_key', openaiApiKey); |
| geminiApiKey = document.getElementById('gemini-api-key').value.trim(); |
| localStorage.setItem('nexus_gemini_key', geminiApiKey); |
| deepseekApiKey = document.getElementById('deepseek-api-key').value.trim(); |
| localStorage.setItem('nexus_deepseek_key', deepseekApiKey); |
| customApiKey = document.getElementById('custom-api-key').value.trim(); |
| localStorage.setItem('nexus_custom_key', customApiKey); |
| customBaseUrl = document.getElementById('custom-base-url').value.trim(); |
| localStorage.setItem('nexus_custom_url', customBaseUrl); |
| customModel = document.getElementById('custom-model').value.trim(); |
| localStorage.setItem('nexus_custom_model', customModel); |
| |
| hfModel = document.getElementById('hf-model').value; |
| localStorage.setItem('nexus_hf_model', hfModel); |
| openaiModel = document.getElementById('openai-model').value; |
| localStorage.setItem('nexus_openai_model', openaiModel); |
| geminiModel = document.getElementById('gemini-model').value; |
| localStorage.setItem('nexus_gemini_model', geminiModel); |
| deepseekModel = document.getElementById('deepseek-model').value; |
| localStorage.setItem('nexus_deepseek_model', deepseekModel); |
| document.getElementById('api-test-result').innerHTML = '✅ Все ключи и настройки сохранены'; |
| } |
| |
| |
| async function askAI(prompt, useGoogleSearch = false) { |
| let apiKey = '', baseURL = '', model = ''; |
| switch(currentProvider) { |
| case 'huggingface': |
| apiKey = hfApiKey; |
| baseURL = 'https://router.huggingface.co/v1'; |
| model = hfModel; |
| break; |
| case 'openai': |
| apiKey = openaiApiKey; |
| baseURL = 'https://api.openai.com/v1'; |
| model = openaiModel; |
| break; |
| case 'gemini': |
| apiKey = geminiApiKey; |
| baseURL = 'https://my-openai-gemini-demo.vercel.app/v1'; |
| model = geminiModel; |
| break; |
| case 'deepseek': |
| apiKey = deepseekApiKey; |
| baseURL = 'https://api.deepseek.com/v1'; |
| model = deepseekModel; |
| break; |
| case 'custom': |
| apiKey = customApiKey; |
| baseURL = customBaseUrl; |
| model = customModel || 'gpt-oss-20b'; |
| break; |
| } |
| if (!apiKey) throw new Error(`🔑 API ключ для ${currentProvider} не указан`); |
| |
| |
| if (currentProvider === 'huggingface') { |
| const response = await fetch(`${baseURL}/chat/completions`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer ${apiKey}` |
| }, |
| body: JSON.stringify({ |
| model: model, |
| messages: [ |
| { role: 'system', content: 'Ты - миниакс помощник Genshin Impact. Отвечай строго по шаблону:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...' }, |
| { role: 'user', content: prompt } |
| ], |
| stream: false |
| }) |
| }); |
| if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || `Ошибка ${response.status}`); } |
| const data = await response.json(); |
| return data.choices[0].message.content; |
| } |
| else if (currentProvider === 'gemini') { |
| const response = await fetch(`${baseURL}/chat/completions`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer ${apiKey}` |
| }, |
| body: JSON.stringify({ |
| model: model, |
| messages: [ |
| { role: 'system', content: 'Ты - миниакс помощник Genshin Impact. Отвечай строго по шаблону:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...' }, |
| { role: 'user', content: prompt } |
| ], |
| temperature: 1 |
| }) |
| }); |
| if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || `Ошибка ${response.status}`); } |
| const data = await response.json(); |
| return data.choices[0].message.content; |
| } |
| else { |
| |
| const response = await fetch(`${baseURL}/chat/completions`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'Authorization': `Bearer ${apiKey}` |
| }, |
| body: JSON.stringify({ |
| model: model, |
| messages: [ |
| { role: 'system', content: 'Ты - миниакс помощник Genshin Impact. Отвечай строго по шаблону:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...' }, |
| { role: 'user', content: prompt } |
| ], |
| temperature: 1 |
| }) |
| }); |
| if (!response.ok) { const err = await response.json(); throw new Error(err.error?.message || `Ошибка ${response.status}`); } |
| const data = await response.json(); |
| return data.choices[0].message.content; |
| } |
| } |
| |
| async function testCurrentApi() { |
| const key = currentProvider === 'huggingface' ? hfApiKey : |
| currentProvider === 'openai' ? openaiApiKey : |
| currentProvider === 'gemini' ? geminiApiKey : |
| currentProvider === 'deepseek' ? deepseekApiKey : customApiKey; |
| if (!key) return alert('🔑 Введите API ключ'); |
| document.getElementById('api-test-result').innerHTML = '⏳ Проверка...'; |
| try { |
| const result = await askAI('Ответь "OK" одной строкой.', false); |
| document.getElementById('api-test-result').innerHTML = '✅ Подключение работает, ключ действителен'; |
| } catch (e) { |
| document.getElementById('api-test-result').innerHTML = `❌ Ошибка: ${e.message}`; |
| } |
| } |
| |
| |
| function getRussianName(engName) { |
| if (nameMapping[engName]) return nameMapping[engName]; |
| if (nameMapping[engName.replace(/[^a-zA-Z]/g, '')]) return nameMapping[engName.replace(/[^a-zA-Z]/g, '')]; |
| const localChar = LOCAL_CHARACTERS.find(c => |
| c.name.toLowerCase().includes(engName.toLowerCase()) || |
| engName.toLowerCase().includes(c.name.toLowerCase().replace(/[^a-zа-я]/g, '')) |
| ); |
| return localChar ? localChar.name : engName; |
| } |
| |
| function getCharacterIcon(charName) { |
| const localChar = LOCAL_CHARACTERS.find(c => c.name === charName); |
| if (localChar && localChar.icon) return localChar.icon; |
| for (let [eng, rus] of Object.entries(nameMapping)) { |
| if (rus === charName || eng === charName) { |
| const found = LOCAL_CHARACTERS.find(c => c.name === rus); |
| if (found) return found.icon; |
| } |
| } |
| return `https://placehold.co/64x64/4A4A4A/FFFFFF?text=${charName.charAt(0)}`; |
| } |
| |
| function getCharacterElement(charName) { |
| const localChar = LOCAL_CHARACTERS.find(c => c.name === charName); |
| if (localChar) return localChar.element.toLowerCase(); |
| for (let [eng, rus] of Object.entries(nameMapping)) { |
| if (rus === charName) { |
| const found = LOCAL_CHARACTERS.find(c => c.name === rus); |
| if (found) return found.element.toLowerCase(); |
| } |
| } |
| return 'anemo'; |
| } |
| |
| |
| function parseAIResponse(text) { |
| const teams = []; |
| const sections = text.split(/\n##\s+/); |
| for (let section of sections) { |
| if (!section.trim()) continue; |
| const title = section.split('\n')[0].trim(); |
| let composition = []; |
| const compMatch = section.match(/\*\*Состав:\*\*\s*(.+)/i); |
| if (compMatch) composition = compMatch[1].split(',').map(s => s.trim()).slice(0,4); |
| let resonance = ''; const resMatch = section.match(/\*\*Резонанс:\*\*\s*(.+)/i); |
| if (resMatch) resonance = resMatch[1].trim(); |
| let reactions = ''; const reactMatch = section.match(/\*\*Реакции:\*\*\s*(.+)/i); |
| if (reactMatch) reactions = reactMatch[1].trim(); |
| let buffs = ''; const buffMatch = section.match(/\*\*Баффы:\*\*\s*(.+)/i); |
| if (buffMatch) buffs = buffMatch[1].trim(); |
| let description = ''; const descMatch = section.match(/\*\*Описание:\*\*\s*(.+)/i); |
| if (descMatch) description = descMatch[1].trim(); |
| teams.push({ title, composition, resonance, reactions, buffs, description }); |
| } |
| return teams; |
| } |
| |
| |
| async function aiTeamBuilder() { |
| if (!localData?.characters?.length) return alert('📁 Сначала загрузите GOOD‑файл'); |
| const resultDiv = document.getElementById('ai-team-result'); |
| resultDiv.innerHTML = '<div style="display:flex; align-items:center; gap:12px;"><div class="loader" style="display:block;"></div> 🧠 AI подбирает отряды...</div>'; |
| const charList = localData.characters.map(c => getRussianName(c.key)).join(', '); |
| const prompt = `У меня есть персонажи: ${charList}. Составь 10 лучших отряда ЖЕЛАТЕЛЬНО c нужными баффами. Используй строгий шаблон:\n## Название отряда\n**Состав:** персонаж1, персонаж2, персонаж3, персонаж4\n**Резонанс:** ...\n**Реакции:** ...\n**Баффы:** ...\n**Описание:** ...`; |
| try { |
| const advice = await askAI(prompt, document.getElementById('googleSearchCheckbox')?.checked || false); |
| const teams = parseAIResponse(advice); |
| if (teams.length === 0) { resultDiv.innerHTML = `<div class="empty">❌ Не удалось распарсить ответ AI</div>`; return; } |
| let html = '<h3 style="color:var(--accent); margin-bottom:24px; font-size:22px;">🧠 AI‑рекомендация отрядов</h3>'; |
| teams.forEach(team => { |
| let resGlow = 'rgba(139,92,246,0.3)'; |
| if (team.resonance.includes('Пиро')) resGlow = 'rgba(255,107,74,0.4)'; |
| else if (team.resonance.includes('Гидро')) resGlow = 'rgba(59,130,246,0.4)'; |
| else if (team.resonance.includes('Крио')) resGlow = 'rgba(56,189,248,0.4)'; |
| else if (team.resonance.includes('Электро')) resGlow = 'rgba(168,85,247,0.4)'; |
| else if (team.resonance.includes('Дендро')) resGlow = 'rgba(132,204,22,0.4)'; |
| else if (team.resonance.includes('Гео')) resGlow = 'rgba(251,191,36,0.4)'; |
| html += `<div class="synergy-card" style="--res-glow: ${resGlow};">`; |
| html += `<div style="display:flex; align-items:center; gap:16px; margin-bottom:16px;"> |
| <span style="font-size:32px;">⚔️</span> |
| <div><h3 style="font-size:22px; font-weight:800;">${team.title}</h3></div> |
| </div>`; |
| html += `<div class="team-members">`; |
| team.composition.forEach(name => { |
| const rusName = getRussianName(name); |
| const element = getCharacterElement(rusName); |
| const elementIcon = elementIcons[element] || '❓'; |
| const elementColor = `var(--${element})`; |
| const icon = getCharacterIcon(rusName); |
| html += `<div class="team-member"> |
| <img src="${icon}" class="member-icon" style="border-color:${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/64x64/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';"> |
| <span style="font-size:14px; font-weight:700;">${rusName}</span> |
| </div>`; |
| }); |
| html += `</div>`; |
| html += `<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px; background:rgba(0,0,0,0.25); border-radius:var(--radius-lg); padding:20px;"> |
| <div><span style="color:var(--accent);">🎵 Резонанс:</span> ${team.resonance || '—'}</div> |
| <div><span style="color:var(--accent);">⚡ Реакции:</span> ${team.reactions || '—'}</div> |
| <div><span style="color:var(--accent);">✨ Баффы:</span> ${team.buffs || '—'}</div> |
| </div>`; |
| if (team.description) html += `<div style="margin-top:16px; color:var(--text-soft); padding:16px; background:rgba(0,0,0,0.15); border-radius:var(--radius-md);">💬 ${team.description}</div>`; |
| html += `<div style="margin-top:16px;"><button class="btn btn-outline" onclick="saveTeamFromAI('${team.title}', ${JSON.stringify(team.composition)})">💾 Сохранить отряд</button></div>`; |
| html += `</div>`; |
| }); |
| resultDiv.innerHTML = html; |
| } catch (e) { |
| resultDiv.innerHTML = `<div style="color:var(--error); padding:20px;">❌ Ошибка: ${e.message}</div>`; |
| } |
| } |
| |
| |
| function saveTeamFromAI(title, composition) { |
| const team = { |
| id: Date.now(), |
| name: title, |
| characters: composition, |
| date: new Date().toLocaleDateString('ru-RU') |
| }; |
| myTeams.push(team); |
| localStorage.setItem('nexus_myteams', JSON.stringify(myTeams)); |
| renderMyTeams(); |
| alert('✅ Отряд сохранён'); |
| } |
| |
| function saveCurrentTeam() { |
| if (!localData?.characters?.length) return alert('📁 Сначала загрузите GOOD‑файл'); |
| const availableChars = localData.characters.map(c => getRussianName(c.key)); |
| if (availableChars.length < 4) return alert('❌ Недостаточно персонажей'); |
| |
| const selectedChars = availableChars.slice(0, 4); |
| const team = { |
| id: Date.now(), |
| name: `Созданный отряд ${new Date().toLocaleDateString('ru-RU')}`, |
| characters: selectedChars, |
| date: new Date().toLocaleDateString('ru-RU') |
| }; |
| myTeams.push(team); |
| localStorage.setItem('nexus_myteams', JSON.stringify(myTeams)); |
| renderMyTeams(); |
| alert('✅ Отряд сохранён'); |
| } |
| |
| |
| function renderMyTeams() { |
| const container = document.getElementById('myteams-grid'); |
| if (!container) return; |
| if (myTeams.length === 0) { |
| container.innerHTML = '<div class="empty">🎲 Нет сохранённых отрядов. Используйте AI‑подбор.</div>'; |
| return; |
| } |
| container.innerHTML = myTeams.map(team => { |
| return `<div class="team-card"> |
| <div class="team-card-header"> |
| <span style="font-size:22px;">⚔️</span> |
| <div><h4 style="font-size:17px; font-weight:700;">${team.name}</h4></div> |
| </div> |
| <div class="team-card-icons"> |
| ${team.characters.map(name => { |
| const rusName = getRussianName(name); |
| const element = getCharacterElement(rusName); |
| const elementColor = `var(--${element})`; |
| const icon = getCharacterIcon(rusName); |
| return `<img src="${icon}" class="team-card-icon" style="border-color:${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/44x44/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';">`; |
| }).join('')} |
| </div> |
| <div style="margin-top:12px; color:var(--text-dim); font-size:12px;">${team.date || ''}</div> |
| <div style="display:flex; gap:8px; margin-top:12px;"> |
| <button class="btn btn-outline" style="padding:4px 12px; font-size:12px;" onclick="deleteTeam(${team.id})">🗑️</button> |
| </div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function deleteTeam(id) { |
| myTeams = myTeams.filter(t => t.id !== id); |
| localStorage.setItem('nexus_myteams', JSON.stringify(myTeams)); |
| renderMyTeams(); |
| } |
| |
| function generateRandomTeam() { |
| if (!localData?.characters?.length) return alert('📁 Сначала загрузите GOOD‑файл'); |
| const availableChars = localData.characters.map(c => getRussianName(c.key)); |
| if (availableChars.length < 4) return alert('❌ Недостаточно персонажей'); |
| const shuffled = [...availableChars].sort(() => 0.5 - Math.random()); |
| const selected = shuffled.slice(0, 4); |
| const team = { |
| id: Date.now(), |
| name: `🎲 Рандомный отряд ${new Date().toLocaleDateString('ru-RU')}`, |
| characters: selected, |
| date: new Date().toLocaleDateString('ru-RU') |
| }; |
| myTeams.push(team); |
| localStorage.setItem('nexus_myteams', JSON.stringify(myTeams)); |
| renderMyTeams(); |
| alert('✅ Рандомный отряд сохранён'); |
| } |
| |
| |
| function createNewProfile() { |
| const name = prompt('Название профиля:', `Профиль ${profiles.length + 1}`); |
| if (!name) return; |
| const profile = { id: Date.now(), name: name, data: localData ? JSON.parse(JSON.stringify(localData)) : null, date: new Date().toLocaleDateString('ru-RU') }; |
| profiles.push(profile); |
| localStorage.setItem('nexus_profiles', JSON.stringify(profiles)); |
| renderProfiles(); |
| } |
| |
| function loadProfile(profileId) { |
| const profile = profiles.find(p => p.id == profileId); |
| if (profile && profile.data) { |
| localData = JSON.parse(JSON.stringify(profile.data)); |
| enrichCharData(); saveToDB(); renderChars(); renderAssessment(); |
| activeProfile = profileId; |
| localStorage.setItem('nexus_active_profile', profileId); |
| alert(`✅ Загружен профиль: ${profile.name}`); |
| } |
| } |
| |
| function deleteProfile(profileId) { |
| if (confirm('Удалить профиль?')) { |
| profiles = profiles.filter(p => p.id !== profileId); |
| localStorage.setItem('nexus_profiles', JSON.stringify(profiles)); |
| renderProfiles(); |
| } |
| } |
| |
| function renderProfiles() { |
| const container = document.getElementById('profiles-list'); |
| if (!container) return; |
| if (profiles.length === 0) { container.innerHTML = '<div class="empty">👤 Нет сохранённых профилей.</div>'; return; } |
| container.innerHTML = profiles.map(p => { |
| const isActive = p.id == activeProfile; |
| return `<div class="profile-badge" style="justify-content:space-between; ${isActive ? 'background:rgba(139,92,246,0.3); border-color:var(--accent);' : ''}"> |
| <div style="display:flex; align-items:center; gap:12px;"> |
| <span style="font-size:20px;">👤</span> |
| <div><span style="font-weight:700;">${p.name}</span> <span style="font-size:12px;">${p.date || ''}</span></div> |
| </div> |
| <div style="display:flex; gap:8px;"> |
| <button class="btn btn-outline" style="padding:6px 12px;" onclick="loadProfile(${p.id})">📂</button> |
| <button class="btn btn-outline" style="padding:6px 12px;" onclick="deleteProfile(${p.id})">🗑️</button> |
| </div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function showProfileSelector() { switchTab('profiles'); } |
| |
| |
| function renderMarkdownWithAnchors(text) { |
| if (!text) return { html: '', toc: '' }; |
| let lines = text.split('\n'), toc = []; |
| let processed = lines.map(line => { |
| let match = line.match(/^(##+)\s+(.+)/); |
| if (match) { |
| let level = match[1].length, title = match[2].trim(), id = title.toLowerCase().replace(/[^\wа-яё\s]/g, '').replace(/\s+/g, '-'); |
| toc.push({ level, title, id }); |
| return `<h${level} id="${id}" style="margin-top:1.5em; color:#c7b9ff;">${title}</h${level}>`; |
| } |
| line = line.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); |
| line = line.replace(/\*(.+?)\*/g, '<em>$1</em>'); |
| line = line.replace(/`(.+?)`/g, '<code>$1</code>'); |
| if (line.trim().startsWith('- ')) return '<li>' + line.trim().substring(2) + '</li>'; |
| if (line.trim().startsWith('* ')) return '<li>' + line.trim().substring(2) + '</li>'; |
| return line; |
| }); |
| let html = '', inList = false; |
| processed.forEach(line => { |
| if (line.startsWith('<li>')) { if (!inList) { html += '<ul>'; inList = true; } html += line; } |
| else { if (inList) { html += '</ul>'; inList = false; } html += line + '<br>'; } |
| }); |
| if (inList) html += '</ul>'; |
| if (toc.length > 1) { |
| let tocHtml = '<div class="toc"><strong>📖 Содержание:</strong><div style="display:flex; flex-wrap:wrap; gap:10px; margin-top:8px;">'; |
| toc.forEach(item => { tocHtml += `<a href="#${item.id}" style="color:var(--accent); background:rgba(139,92,246,0.1); padding:4px 12px; border-radius:40px;">${item.title}</a>`; }); |
| tocHtml += '</div></div>'; |
| return { html: tocHtml + html, toc: tocHtml }; |
| } |
| return { html, toc: '' }; |
| } |
| |
| |
| async function analyzeCharacter(charKey) { |
| if (!localData) return alert('📁 Сначала загрузите GOOD‑файл.'); |
| const char = localData.characters.find(c => c.key === charKey); |
| if (!char) return; |
| const arts = localData.artifacts.filter(a => a.location === charKey); |
| const weapon = localData.weapons?.find(w => w.location === charKey) || { key: '—', level: 0, refinement: 0 }; |
| document.getElementById('ai-overlay').classList.add('open'); |
| document.getElementById('ai-text').innerHTML = '<div style="display:flex; align-items:center; gap:16px; padding:40px;"><div class="loader"></div> ⏳ Анализ...</div>'; |
| document.getElementById('ai-toc').style.display = 'none'; |
| const role = roles[charKey] || 'Универсал'; |
| const element = charElements[charKey] || '—'; |
| const rusName = getRussianName(charKey); |
| const context = `Персонаж: ${rusName} (${charKey}), уровень ${char.level}, созвездие ${char.constellation}, элемент ${element}. Роль: ${role}. Оружие: ${weapon.key} (ур.${weapon.level}, Проб.${weapon.refinement}). Таланты: атака ${char.talent?.auto || 0}, навык ${char.talent?.skill || 0}, взрыв ${char.talent?.burst || 0}. Артефакты (${arts.length}): ${JSON.stringify(arts.map(a => ({ сет: artifactSetNames[a.setKey] || a.setKey, осн: a.mainStatKey, суб: a.substats })))}. |
| Напиши подробный анализ на русском: 1. Оценка билда (что хорошо/плохо) — 2-3 предложения. 2. 3 конкретных совета по улучшению. 3. Идеальные целевые значения CR, CD, ER, EM. Используй заголовки ## и ###, списки.`; |
| try { |
| const advice = await askAI(context, false); |
| const rendered = renderMarkdownWithAnchors(advice); |
| document.getElementById('ai-text').innerHTML = rendered.html; |
| if (rendered.toc) { document.getElementById('ai-toc').innerHTML = rendered.toc; document.getElementById('ai-toc').style.display = 'block'; } |
| else { document.getElementById('ai-toc').style.display = 'none'; } |
| document.getElementById('ai-text').scrollTop = 0; |
| } catch (e) { |
| document.getElementById('ai-text').innerHTML = `<div style="color:var(--error); padding:24px;">❌ Ошибка: ${e.message}</div>`; |
| } |
| } |
| |
| function closeAI() { document.getElementById('ai-overlay').classList.remove('open'); } |
| |
| |
| function switchTab(tabId) { |
| document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); |
| document.querySelector(`[data-tab="${tabId}"]`).classList.add('active'); |
| document.querySelectorAll('.view-panel').forEach(el => el.classList.remove('active')); |
| const panel = document.getElementById(`view-${tabId}`); |
| if (panel) panel.classList.add('active'); |
| if (tabId === 'artifacts' && localData) renderArtifacts(); |
| if (tabId === 'team' && localData) renderTeams(); |
| if (tabId === 'chars' && localData) renderChars(); |
| if (tabId === 'assessment' && localData) renderAssessment(); |
| if (tabId === 'myteams') renderMyTeams(); |
| if (tabId === 'profiles') renderProfiles(); |
| } |
| |
| |
| const dbReq = indexedDB.open("NexusDB", 4); |
| dbReq.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains('app')) db.createObjectStore("app", { keyPath: "id" }); }; |
| dbReq.onsuccess = e => { dbInstance = e.target.result; loadFromDB(); }; |
| function loadFromDB() { |
| if (!dbInstance) return; |
| const tx = dbInstance.transaction("app", "readonly"); |
| const store = tx.objectStore("app"); |
| const req = store.get("state"); |
| req.onsuccess = () => { if (req.result) { localData = req.result.data; enrichCharData(); renderChars(); renderAssessment(); } }; |
| } |
| function saveToDB() { if (dbInstance && localData) dbInstance.transaction("app", "readwrite").objectStore("app").put({ id: "state", data: localData }); } |
| function clearDB() { if (confirm('Удалить все данные?')) { dbInstance.transaction("app", "readwrite").objectStore("app").clear(); localData = null; renderChars(); location.reload(); } } |
| |
| |
| function enrichCharData() { |
| if (!localData?.characters) return; |
| localData.characters.forEach(c => { |
| charElements[c.key] = elementMap[c.key] || 'anemo'; |
| if (!roles[c.key]) { |
| if (['Bennett','Diona','Zhongli','Kazuha','Sucrose','Gorou','Faruzan','Chevreuse','Mika','Layla','Kirara','Yaoyao','Barbara','Jean','Sayu','Charlotte','Xianyun','KukiShinobu','Citlali','Xilonen','Furina','Kokomi','Shenhe'].includes(c.key)) |
| roles[c.key] = 'support'; |
| else if (['Xingqiu','Yelan','Fischl','Xiangling','Nahida','Alhaitham','Rosaria','Kaeya','Beidou','Collei','KujouSara','YunJin','Candace','Ororon','Thoma','Dehya','Kaveh','Gaming','Chiori','Columbina','Aino','Skirk','Dahlia','Clorinde','Emilie','Kinich','Neuvillette'].includes(c.key)) |
| roles[c.key] = 'subdps'; |
| else roles[c.key] = 'dps'; |
| } |
| }); |
| } |
| |
| |
| function getCV(art) { |
| if (!art.substats) return 0; |
| return art.substats.reduce((acc, s) => { |
| if (s.key === 'critRate_') return acc + s.value * 2; |
| if (s.key === 'critDMG_') return acc + s.value; |
| return acc; |
| }, 0); |
| } |
| |
| |
| function renderChars() { |
| if (!localData) { document.getElementById('empty-msg').style.display = 'block'; return; } |
| renderTable(); renderCards(); |
| if (charsView === 'table') { |
| document.getElementById('chars-table-container').style.display = 'block'; |
| document.getElementById('chars-cards-container').style.display = 'none'; |
| } else { |
| document.getElementById('chars-table-container').style.display = 'none'; |
| document.getElementById('chars-cards-container').style.display = 'grid'; |
| } |
| document.getElementById('empty-msg').style.display = 'none'; |
| document.getElementById('total-c').innerText = localData.characters?.length || 0; |
| document.getElementById('total-a').innerText = localData.artifacts?.length || 0; |
| } |
| |
| function renderTable() { |
| const chars = localData.characters || [], arts = localData.artifacts || [], weapons = localData.weapons || []; |
| let list = chars.map(c => { |
| const equippedArts = arts.filter(a => a.location === c.key); |
| const cv = equippedArts.reduce((sum, a) => sum + getCV(a), 0); |
| const weapon = weapons.find(w => w.location === c.key); |
| return { ...c, equipped: equippedArts, cv, weapon }; |
| }); |
| const elFilter = document.getElementById('f-el').value; |
| if (elFilter !== 'all') list = list.filter(c => charElements[c.key] === elFilter); |
| const sort = document.getElementById('f-sort').value; |
| if (sort === 'cv') list.sort((a,b) => b.cv - a.cv); |
| else if (sort === 'lvl') list.sort((a,b) => b.level - a.level); |
| else if (sort === 'name') list.sort((a,b) => getRussianName(a.key).localeCompare(getRussianName(b.key), 'ru')); |
| const tbody = document.getElementById('char-body'); tbody.innerHTML = ''; |
| let totalCv = 0; |
| list.forEach(c => { |
| totalCv += c.cv; |
| const tr = document.createElement('tr'); tr.className = 'data-row'; |
| const element = charElements[c.key] || 'anemo'; |
| const elementColor = `var(--${element})`; |
| const elementIcon = elementIcons[element] || ''; |
| const rusName = getRussianName(c.key); |
| const icon = getCharacterIcon(rusName); |
| const setNames = [...new Set(c.equipped.map(a => artifactSetNames[a.setKey] || a.setKey))]; |
| const setsDisplay = setNames.length ? setNames.join(', ') : '—'; |
| let weaponDisplay = '—'; if (c.weapon) weaponDisplay = `${c.weapon.key} 🔹 Проб.${c.weapon.refinement} ур.${c.weapon.level}`; |
| let roleDisplay = '—'; if (roles[c.key]) roleDisplay = roleNames[roles[c.key]] || roles[c.key]; |
| let cvStatus = '', cvColor = ''; |
| if (c.cv > 200) { cvStatus = 'Высокий'; cvColor = 'var(--success)'; } |
| else if (c.cv > 140) { cvStatus = 'Средний'; cvColor = 'var(--warning)'; } |
| else { cvStatus = 'Низкий'; cvColor = 'var(--error)'; } |
| tr.innerHTML = ` |
| <td><div style="display:flex; align-items:center; gap:12px;"> |
| <img src="${icon}" style="width:44px; height:44px; border-radius:50%; border:2px solid ${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/44x44/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';"> |
| <div><a href="https://www.google.com/search?q=Genshin+Impact+${encodeURIComponent(rusName)}+гайд" target="_blank" style="font-size:16px; font-weight:700; color:#fff;">${rusName}</a> |
| <span style="background:rgba(0,0,0,0.4); padding:4px 10px; border-radius:30px; font-size:12px; margin-left:8px;">Ур.${c.level} · С${c.constellation}</span></div> |
| </div></td> |
| <td><span class="weapon-badge">⚔️ ${weaponDisplay}</span><br><span class="role-badge" style="margin-top:6px;">🎯 ${roleDisplay}</span></td> |
| <td><span style="color:var(--text-soft);">${setsDisplay}</span></td> |
| <td><span style="font-size:18px; font-weight:800;">${c.cv.toFixed(1)}</span> |
| <div class="cv-meter"><div class="cv-fill" style="width:${Math.min(c.cv/2.2,100)}%; background:${cvColor};"></div></div> |
| <span style="font-size:11px; color:${cvColor};">${cvStatus}</span> |
| </td> |
| <td><button class="ai-btn" onclick="analyzeCharacter('${c.key}')">✨ Анализ</button></td> |
| `; |
| tbody.appendChild(tr); |
| }); |
| document.getElementById('avg-cv').innerText = (totalCv / (list.length || 1)).toFixed(1); |
| } |
| |
| function renderCards() { |
| const chars = localData.characters || [], arts = localData.artifacts || [], weapons = localData.weapons || []; |
| let list = chars.map(c => { |
| const equippedArts = arts.filter(a => a.location === c.key); |
| const cv = equippedArts.reduce((sum, a) => sum + getCV(a), 0); |
| const weapon = weapons.find(w => w.location === c.key); |
| return { ...c, equipped: equippedArts, cv, weapon }; |
| }); |
| const elFilter = document.getElementById('f-el').value; |
| if (elFilter !== 'all') list = list.filter(c => charElements[c.key] === elFilter); |
| const sort = document.getElementById('f-sort').value; |
| if (sort === 'cv') list.sort((a,b) => b.cv - a.cv); |
| else if (sort === 'lvl') list.sort((a,b) => b.level - a.level); |
| else if (sort === 'name') list.sort((a,b) => getRussianName(a.key).localeCompare(getRussianName(b.key), 'ru')); |
| const container = document.getElementById('chars-cards-container'); container.innerHTML = ''; |
| list.forEach(c => { |
| const element = charElements[c.key] || 'anemo'; |
| const elementColor = `var(--${element})`; |
| const elementGlow = `var(--${element}-glow)`; |
| const rusName = getRussianName(c.key); |
| const icon = getCharacterIcon(rusName); |
| const setNames = [...new Set(c.equipped.map(a => artifactSetNames[a.setKey] || a.setKey))]; |
| const setsDisplay = setNames.length ? setNames.join(', ') : '—'; |
| let weaponDisplay = '—'; if (c.weapon) weaponDisplay = `${c.weapon.key} 🔹 Проб.${c.weapon.refinement}`; |
| let roleDisplay = '—'; if (roles[c.key]) roleDisplay = roleNames[roles[c.key]] || roles[c.key]; |
| let cvStatus = '', cvColor = ''; |
| if (c.cv > 200) { cvStatus = 'Высокий'; cvColor = 'var(--success)'; } |
| else if (c.cv > 140) { cvStatus = 'Средний'; cvColor = 'var(--warning)'; } |
| else { cvStatus = 'Низкий'; cvColor = 'var(--error)'; } |
| const card = document.createElement('div'); card.className = 'char-card'; |
| card.style.setProperty('--element-color', elementColor); |
| card.style.setProperty('--element-glow', elementGlow); |
| card.innerHTML = ` |
| <div class="char-card-header"> |
| <img src="${icon}" class="char-element-icon" style="border-color:${elementColor}; object-fit:cover;" onerror="this.onerror=null;this.src='https://placehold.co/60x60/4A4A4A/FFFFFF?text=${rusName.charAt(0)}';"> |
| <div><div class="char-name"><a href="https://www.google.com/search?q=Genshin+Impact+${encodeURIComponent(rusName)}+гайд" target="_blank">${rusName}</a></div> |
| <span class="char-level">⭐ Ур.${c.level} · Созвездие ${c.constellation}</span></div> |
| </div> |
| <div class="char-card-stats"><span class="weapon-badge">⚔️ ${weaponDisplay}</span><span class="role-badge">🎯 ${roleDisplay}</span></div> |
| <div class="char-card-sets"><span style="color:var(--text-dim);">⚙️ Сеты:</span> ${setsDisplay}</div> |
| <div style="display:flex; justify-content:space-between; align-items:center;"> |
| <div><span style="font-size:22px; font-weight:800;">${c.cv.toFixed(1)}</span> |
| <span style="font-size:12px; color:${cvColor}; margin-left:6px;">${cvStatus}</span> |
| <div class="cv-meter" style="width:100px;"><div class="cv-fill" style="width:${Math.min(c.cv/2.2,100)}%; background:${cvColor};"></div></div></div> |
| <button class="ai-btn" onclick="analyzeCharacter('${c.key}')">✨ Анализ</button> |
| </div> |
| `; |
| container.appendChild(card); |
| }); |
| } |
| |
| |
| function renderArtifacts() { |
| if (!localData?.artifacts) { document.getElementById('artifacts-list').innerHTML = '<div class="empty">Нет артефактов</div>'; return; } |
| |
| |
| const setFilter = document.getElementById('art-set-filter'); |
| setFilter.innerHTML = '<option value="all">Все сеты</option>'; |
| const uniqueSets = [...new Set(localData.artifacts.map(a => a.setKey))]; |
| uniqueSets.sort().forEach(setKey => { |
| const setName = artifactSetNames[setKey] || setKey; |
| const option = document.createElement('option'); |
| option.value = setKey; |
| option.textContent = setName; |
| setFilter.appendChild(option); |
| }); |
| |
| let filtered = localData.artifacts; |
| const setFilterValue = document.getElementById('art-set-filter').value; |
| if (setFilterValue !== 'all') filtered = filtered.filter(a => a.setKey === setFilterValue); |
| const slotFilter = document.getElementById('art-slot-filter').value; |
| if (slotFilter !== 'all') filtered = filtered.filter(a => a.slotKey === slotFilter); |
| const groupBy = document.getElementById('art-set-group').value; |
| const container = document.getElementById('artifacts-list'); |
| |
| if (groupBy !== 'all') { |
| |
| let groups = {}; |
| if (groupBy === 'set') { |
| filtered.forEach(art => { const key = art.setKey; if (!groups[key]) groups[key] = []; groups[key].push(art); }); |
| } else if (groupBy === 'slot') { |
| filtered.forEach(art => { const key = art.slotKey; if (!groups[key]) groups[key] = []; groups[key].push(art); }); |
| } else if (groupBy === 'rarity') { |
| filtered.forEach(art => { const key = art.rarity || 5; if (!groups[key]) groups[key] = []; groups[key].push(art); }); |
| } else if (groupBy === 'level') { |
| filtered.forEach(art => { const key = Math.floor(art.level/4)*4; if (!groups[key]) groups[key] = []; groups[key].push(art); }); |
| } else if (groupBy === 'cv') { |
| |
| filtered.forEach(art => { |
| const cv = getCV(art); |
| let key; |
| if (cv > 200) key = 'Высокий (>200)'; |
| else if (cv > 140) key = 'Средний (140-200)'; |
| else key = 'Низкий (<140)'; |
| if (!groups[key]) groups[key] = []; |
| groups[key].push(art); |
| }); |
| } |
| let html = ''; |
| for (let key in groups) { |
| let groupName = key; |
| if (groupBy === 'set') groupName = artifactSetNames[key] || key; |
| if (groupBy === 'slot') groupName = slotNames[key] || key; |
| if (groupBy === 'rarity') groupName = `${key}★`; |
| if (groupBy === 'level') groupName = `+${key} и выше`; |
| if (groupBy === 'cv') groupName = key; |
| html += `<h3 style="margin:16px 0 8px; color:var(--text-soft);">${groupName} (${groups[key].length})</h3>`; |
| html += renderArtifactList(groups[key]); |
| } |
| container.innerHTML = html; |
| } else { |
| container.innerHTML = renderArtifactList(filtered); |
| } |
| } |
| |
| function renderArtifactList(artifacts) { |
| if (artifactsView === 'list') { |
| return artifacts.map(art => { |
| const cv = getCV(art); |
| const loc = art.location ? getRussianName(art.location) : '❌ не надет'; |
| const setRus = artifactSetNames[art.setKey] || art.setKey; |
| const slotRus = slotNames[art.slotKey] || art.slotKey; |
| const mainStatRus = statNames[art.mainStatKey] || art.mainStatKey; |
| return `<div class="artifact-card" style="flex-direction:column; align-items:stretch;"> |
| <div style="display:flex; justify-content:space-between;"><div><b>${setRus}</b> · ${slotRus}</div><div class="artifact-mainstat">${mainStatRus} +${art.level}</div></div> |
| <div style="display:flex; gap:10px; flex-wrap:wrap;">${art.substats.map(s => `<span style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1); padding:6px 14px; border-radius:40px; font-size:12px;">${statNames[s.key] || s.key} +${s.value}</span>`).join('')}</div> |
| <div style="display:flex; justify-content:space-between; color:var(--text-dim);"><span>CV: <b>${cv.toFixed(1)}</b></span><span>📌 ${loc}</span></div> |
| </div>`; |
| }).join(''); |
| } else { |
| return artifacts.map(art => { |
| const cv = getCV(art); |
| const loc = art.location ? getRussianName(art.location) : '❌ не надет'; |
| const setRus = artifactSetNames[art.setKey] || art.setKey; |
| const slotRus = slotNames[art.slotKey] || art.slotKey; |
| const mainStatRus = statNames[art.mainStatKey] || art.mainStatKey; |
| return `<div class="artifact-card"> |
| <div style="min-width:160px;"><b>${setRus}</b><br><span style="color:var(--text-dim);">${slotRus}</span></div> |
| <div class="artifact-mainstat">${mainStatRus} +${art.level}</div> |
| <div style="flex:1; display:flex; gap:8px; flex-wrap:wrap;">${art.substats.map(s => `<span style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.1); padding:6px 12px; border-radius:40px; font-size:11px;">${statNames[s.key] || s.key} +${s.value}</span>`).join('')}</div> |
| <div>CV: <b>${cv.toFixed(1)}</b></div> |
| <div style="color:var(--text-dim);">📌 ${loc}</div> |
| </div>`; |
| }).join(''); |
| } |
| } |
| |
| function groupArtifacts() { renderArtifacts(); } |
| |
| |
| function renderTeams() { |
| if (!localData?.characters) { document.getElementById('team-suggestions').innerHTML = '<div class="empty">Сначала загрузите персонажей.</div>'; return; } |
| const owned = localData.characters.map(c => c.key); |
| const container = document.getElementById('team-suggestions'); |
| container.innerHTML = metaTeams.map(t => { |
| const hasChars = t.chars.map(c => owned.includes(c)); |
| const ownedCount = hasChars.filter(Boolean).length; |
| const missing = t.chars.filter((c,i) => !hasChars[i]).map(c => getRussianName(c)); |
| let resGlow = 'rgba(139,92,246,0.3)'; |
| if (t.resonance.includes('Пиро')) resGlow = 'rgba(255,107,74,0.4)'; |
| else if (t.resonance.includes('Гидро')) resGlow = 'rgba(59,130,246,0.4)'; |
| else if (t.resonance.includes('Крио')) resGlow = 'rgba(56,189,248,0.4)'; |
| else if (t.resonance.includes('Электро')) resGlow = 'rgba(168,85,247,0.4)'; |
| else if (t.resonance.includes('Дендро')) resGlow = 'rgba(132,204,22,0.4)'; |
| else if (t.resonance.includes('Гео')) resGlow = 'rgba(251,191,36,0.4)'; |
| return `<div class="synergy-card" style="--res-glow: ${resGlow};"> |
| <div style="display:flex; align-items:center; gap:12px;"><span style="font-size:32px;">${t.name.split(' ')[0]}</span> |
| <div><h3 style="font-size:20px;">${t.fullName}</h3><span style="background:rgba(139,92,246,0.2); padding:4px 12px; border-radius:40px; font-size:12px;">Мета ${t.meta}</span></div></div> |
| <div style="display:flex; gap:16px; flex-wrap:wrap; margin:16px 0;"> |
| ${t.chars.map((c,i) => { const ownedHere = hasChars[i]; const rusName = getRussianName(c); |
| return `<span style="display:inline-flex; align-items:center; gap:8px; background:${ownedHere ? 'rgba(16,185,129,0.15)' : 'rgba(239,68,68,0.15)'}; border:1px solid ${ownedHere ? 'rgba(16,185,129,0.3)' : 'rgba(239,68,68,0.3)'}; padding:6px 16px; border-radius:40px;">${rusName} ${ownedHere ? '✅' : '❌'}</span>`; }).join('')} |
| </div> |
| <div style="display:grid; grid-template-columns:repeat(3,1fr); gap:16px; background:rgba(0,0,0,0.25); border-radius:var(--radius-md); padding:16px;"> |
| <div><span style="color:var(--accent);">🎵 Резонанс:</span> ${t.resonance}</div> |
| <div><span style="color:var(--accent);">⚡ Реакции:</span> ${t.reactions}</div> |
| <div><span style="color:var(--accent);">✨ Баффы:</span> ${t.buffs}</div> |
| </div> |
| <div style="margin-top:16px; color:var(--text-soft); background:rgba(0,0,0,0.15); padding:16px; border-radius:var(--radius-md);">💬 ${t.description}</div> |
| <div style="margin-top:16px; display:flex; justify-content:space-between; align-items:center;"> |
| <span>⭐ В наличии: <b>${ownedCount}/${t.chars.length}</b> ${missing.length ? `❌ Нет: ${missing.join(', ')}` : '✅ Полный отряд'}</span> |
| <button class="btn btn-outline" onclick="saveTeamFromAI('${t.fullName}', ${JSON.stringify(t.chars)})">💾 Сохранить</button> |
| </div> |
| </div>`; |
| }).join(''); |
| } |
| |
| |
| function renderAssessment() { |
| if (!localData?.characters) { document.getElementById('assessment-content').innerHTML = '<div class="empty">Загрузите GOOD‑файл.</div>'; return; } |
| const chars = localData.characters; |
| const arts = localData.artifacts || []; |
| const chars80 = chars.filter(c => c.level >= 80).length; |
| const chars90 = chars.filter(c => c.level >= 90).length; |
| let totalCV = 0, cvCount = 0; |
| chars.forEach(c => { const equipped = arts.filter(a => a.location === c.key); const charCV = equipped.reduce((s, a) => s + getCV(a), 0); if (equipped.length > 0) { totalCV += charCV; cvCount++; } }); |
| const avgCV = cvCount > 0 ? totalCV / cvCount : 0; |
| const keySupports = ['Zhongli', 'Bennett', 'Kazuha', 'Nahida', 'Furina', 'Kokomi', 'Xingqiu', 'Yelan']; |
| const keyDPS = ['Neuvillette', 'Arlecchino', 'Alhaitham', 'HuTao', 'RaidenShogun', 'Wriothesley', 'Clorinde', 'Navia', 'Ganyu', 'Xiao']; |
| const ownedSupports = chars.filter(c => keySupports.includes(c.key)).length; |
| const ownedDPS = chars.filter(c => keyDPS.includes(c.key)).length; |
| const arts20 = arts.filter(a => a.level >= 20).length; |
| let abyssScore = 0; |
| if (chars80 >= 12) abyssScore += 25; else if (chars80 >= 10) abyssScore += 20; else if (chars80 >= 8) abyssScore += 15; else abyssScore += 5; |
| if (ownedSupports >= 6) abyssScore += 25; else if (ownedSupports >= 4) abyssScore += 20; else if (ownedSupports >= 2) abyssScore += 10; |
| if (ownedDPS >= 4) abyssScore += 20; else if (ownedDPS >= 2) abyssScore += 15; else if (ownedDPS >= 1) abyssScore += 8; |
| if (avgCV > 180) abyssScore += 15; else if (avgCV > 160) abyssScore += 12; else if (avgCV > 140) abyssScore += 8; else if (avgCV > 120) abyssScore += 4; |
| if (arts20 >= 16) abyssScore += 15; else if (arts20 >= 12) abyssScore += 10; else if (arts20 >= 8) abyssScore += 5; |
| abyssScore = Math.min(100, abyssScore); |
| let abyssResult = abyssScore >= 90 ? '⭐ 36 ЗВЁЗД' : abyssScore >= 80 ? '✨ 35-36 звёзд' : abyssScore >= 70 ? '⚔️ 33-35 звёзд' : abyssScore >= 60 ? '🛡️ 30-33 звезды' : abyssScore >= 50 ? '⚠️ 27-30 звёзд' : '❌ Менее 27 звёзд'; |
| let mgScore = 0; |
| const mgDPS = ['Arlecchino', 'Neuvillette', 'Navia', 'Clorinde', 'Wriothesley', 'HuTao', 'Alhaitham']; |
| const ownedMgDPS = chars.filter(c => mgDPS.includes(c.key)).length; |
| if (ownedMgDPS >= 2) mgScore += 40; else if (ownedMgDPS >= 1) mgScore += 20; |
| if (chars90 >= 4) mgScore += 30; else if (chars90 >= 2) mgScore += 20; else mgScore += 10; |
| if (avgCV > 170) mgScore += 30; else if (avgCV > 150) mgScore += 20; else mgScore += 5; |
| mgScore = Math.min(100, mgScore); |
| let mgResult = mgScore >= 80 ? '✅ Мрачный натиск — ПРОЙДЕН' : mgScore >= 65 ? '⚠️ Мрачный натиск — возможно, но сложно' : '❌ Мрачный натиск — НЕ ПРОЙТИ'; |
| const html = ` |
| <div class="assessment-card" style="border-left-color: var(--accent);"> |
| <h3 style="font-size:24px;">📊 Общая статистика</h3> |
| <div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(180px,1fr)); gap:20px; margin-top:20px;"> |
| <div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Персонажи</div><div style="font-size:36px; font-weight:800;">${chars.length}</div></div> |
| <div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Уровень 80+</div><div style="font-size:36px; font-weight:800;">${chars80}</div><div style="font-size:13px;">90 ур: ${chars90}</div></div> |
| <div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Средний CV</div><div style="font-size:36px; font-weight:800;">${avgCV.toFixed(1)}</div></div> |
| <div style="background:rgba(0,0,0,0.2); padding:20px; border-radius:var(--radius-lg);"><div style="font-size:14px; color:var(--text-dim);">Артефакты +20</div><div style="font-size:36px; font-weight:800;">${arts20}</div></div> |
| </div> |
| </div> |
| <div class="assessment-card" style="border-left-color: var(--pyro);"> |
| <div style="display:flex; justify-content:space-between;"><h3 style="font-size:24px;">⚔️ Бездна 12-3</h3><span style="font-size:42px; font-weight:800; color:${abyssScore>=80?'var(--success)':abyssScore>=60?'var(--warning)':'var(--error)'};">${abyssScore}</span></div> |
| <div style="font-size:22px; font-weight:800; margin:12px 0 4px;">${abyssResult}</div> |
| <div style="background:rgba(0,0,0,0.2); border-radius:var(--radius-md); padding:16px; margin-top:16px;"> |
| <div>🎯 Топ-саппорты: ${ownedSupports}/8</div> |
| <div>⚡ Топ-ДД: ${ownedDPS}/10</div> |
| <div>⭐ CV: ${avgCV > 160 ? 'отличный' : avgCV > 140 ? 'хороший' : avgCV > 120 ? 'средний' : 'низкий'}</div> |
| </div> |
| </div> |
| <div class="assessment-card" style="border-left-color: var(--cryo);"> |
| <div style="display:flex; justify-content:space-between;"><h3 style="font-size:24px;">🌑 Мрачный натиск</h3><span style="font-size:42px; font-weight:800; color:${mgScore>=80?'var(--success)':mgScore>=65?'var(--warning)':'var(--error)'};">${mgScore}</span></div> |
| <div style="font-size:22px; font-weight:800; margin:12px 0 4px;">${mgResult}</div> |
| <div style="margin-top:16px; color:var(--text-dim);">💡 Рекомендованы: Арлекино, Нёвиллет, Навия, Клоринда, Ризли</div> |
| </div> |
| `; |
| document.getElementById('assessment-content').innerHTML = html; |
| } |
| |
| |
| function applyFilters() { renderChars(); } |
| function setCharsView(view) { charsView = view; localStorage.setItem('nexus_chars_view', view); document.querySelectorAll('#view-chars .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)); renderChars(); } |
| function setArtifactsView(view) { artifactsView = view; localStorage.setItem('nexus_artifacts_view', view); document.querySelectorAll('#view-artifacts .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view)); renderArtifacts(); } |
| |
| function changeTheme(theme) { |
| if (theme === 'light') { |
| document.documentElement.style.setProperty('--bg-main', '#f3f4f6'); |
| document.documentElement.style.setProperty('--bg-side', '#e5e7eb'); |
| document.documentElement.style.setProperty('--bg-card', '#ffffff'); |
| document.documentElement.style.setProperty('--text-main', '#111827'); |
| document.documentElement.style.setProperty('--text-dim', '#4b5563'); |
| } |
| else if (theme === 'darker') { |
| document.documentElement.style.setProperty('--bg-main', '#030712'); |
| document.documentElement.style.setProperty('--bg-side', '#0a0c12'); |
| document.documentElement.style.setProperty('--bg-card', '#111827'); |
| } |
| else { |
| document.documentElement.style.setProperty('--bg-main', '#0a0c12'); |
| document.documentElement.style.setProperty('--bg-side', '#0f1117'); |
| document.documentElement.style.setProperty('--bg-card', '#1a1e2a'); |
| document.documentElement.style.setProperty('--text-main', '#ffffff'); |
| document.documentElement.style.setProperty('--text-dim', '#a0a8c0'); |
| } |
| } |
| |
| |
| document.getElementById('fileIn').onchange = e => { |
| const file = e.target.files[0]; if (!file) return; |
| const reader = new FileReader(); |
| reader.onload = ev => { |
| try { |
| const newData = JSON.parse(ev.target.result); |
| if (confirm('Сохранить в новый профиль? (Отмена — заменить текущие)')) { |
| const profileName = prompt('Название профиля:', `Профиль ${profiles.length + 1}`); |
| if (profileName) { profiles.push({ id: Date.now(), name: profileName, data: newData, date: new Date().toLocaleDateString('ru-RU') }); localStorage.setItem('nexus_profiles', JSON.stringify(profiles)); } |
| } |
| localData = newData; enrichCharData(); saveToDB(); renderChars(); |
| if (localData.artifacts) { |
| const sets = [...new Set(localData.artifacts.map(a => a.setKey))].sort(); |
| const setSelect = document.getElementById('art-set-filter'); |
| setSelect.innerHTML = '<option value="all">Все сеты</option>' + sets.map(s => `<option value="${s}">${artifactSetNames[s] || s}</option>`).join(''); |
| } |
| switchTab('chars'); renderAssessment(); alert('✅ Данные загружены!'); |
| } catch(ex) { alert('❌ Неверный GOOD‑файл'); } |
| }; |
| reader.readAsText(file); |
| }; |
| |
| |
| function exportGOOD() { if (!localData) return alert('Нет данных'); const dataStr = JSON.stringify(localData, null, 2); const blob = new Blob([dataStr], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'genshin_nexus_export.json'; a.click(); } |
| |
| |
| function initSettings() { |
| |
| document.getElementById('hf-api-key').value = hfApiKey; |
| document.getElementById('openai-api-key').value = openaiApiKey; |
| document.getElementById('gemini-api-key').value = geminiApiKey; |
| document.getElementById('deepseek-api-key').value = deepseekApiKey; |
| document.getElementById('custom-api-key').value = customApiKey; |
| document.getElementById('custom-base-url').value = customBaseUrl; |
| document.getElementById('custom-model').value = customModel; |
| document.getElementById('hf-model').value = hfModel; |
| document.getElementById('openai-model').value = openaiModel; |
| document.getElementById('gemini-model').value = geminiModel; |
| document.getElementById('deepseek-model').value = deepseekModel; |
| |
| setAIProvider(currentProvider); |
| |
| document.querySelectorAll('#view-chars .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === charsView)); |
| document.querySelectorAll('#view-artifacts .view-toggle-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.view === artifactsView)); |
| renderProfiles(); renderMyTeams(); |
| } |
| |
| |
| window.switchTab = switchTab; |
| window.analyzeCharacter = analyzeCharacter; |
| window.aiTeamBuilder = aiTeamBuilder; |
| window.closeAI = closeAI; |
| window.clearDB = clearDB; |
| window.renderArtifacts = renderArtifacts; |
| window.renderTeams = renderTeams; |
| window.saveAllApiKeys = saveAllApiKeys; |
| window.testCurrentApi = testCurrentApi; |
| window.setAIProvider = setAIProvider; |
| window.exportGOOD = exportGOOD; |
| window.applyFilters = applyFilters; |
| window.setCharsView = setCharsView; |
| window.setArtifactsView = setArtifactsView; |
| window.renderChars = renderChars; |
| window.openLegend = openLegend; |
| window.closeLegend = closeLegend; |
| window.openCVPopup = openCVPopup; |
| window.closeCVPopup = closeCVPopup; |
| window.renderAssessment = renderAssessment; |
| window.changeTheme = changeTheme; |
| window.generateRandomTeam = generateRandomTeam; |
| window.saveTeamFromAI = saveTeamFromAI; |
| window.saveCurrentTeam = saveCurrentTeam; |
| window.deleteTeam = deleteTeam; |
| window.renderMyTeams = renderMyTeams; |
| window.createNewProfile = createNewProfile; |
| window.loadProfile = loadProfile; |
| window.deleteProfile = deleteProfile; |
| window.renderProfiles = renderProfiles; |
| window.showProfileSelector = showProfileSelector; |
| window.groupArtifacts = groupArtifacts; |
| |
| document.addEventListener('DOMContentLoaded', initSettings); |
| </script> |
| </body> |
| </html> |