2b399fdb81
PR #1223 only staged the deletions of the old paths because the new top-level directories were still untracked when the commit was authored. This commit adds the actual restructured tree: SREBOT/ (existing bot), SHARED/ (vromfs, data_parser, ICONS/MAPS/FONTS, DAGOR_FILES, update_game_files), and TSSBOT/ (skeleton). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1622 lines
70 KiB
Plaintext
1622 lines
70 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="<%= lang %>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title><%= t('leaderboard.comparisonTitle') %> | <%= botName %></title>
|
|
<meta name="description" content="<%= t('leaderboard.comparisonSubtitle') %>">
|
|
<link rel="icon" type="image/png" href="/images/transparent_toothlessssss.png">
|
|
|
|
<!-- Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
|
<link rel="preload" href="/Fonts/symbols_skyquake.ttf" as="font" type="font/ttf" crossorigin="anonymous">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
|
|
<!-- Tailwind CSS -->
|
|
<link rel="stylesheet" href="/css/output.css">
|
|
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" media="print" onload="this.media='all'">
|
|
|
|
<style>
|
|
@font-face {
|
|
font-family: 'skyquakesymbols';
|
|
src: url('/Fonts/symbols_skyquake.ttf') format('truetype');
|
|
font-display: swap;
|
|
}
|
|
|
|
body {
|
|
background: #1b1b1b;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.text-accent { color: #F5F5DC; }
|
|
.text-muted { color: #90EE90; }
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%);
|
|
box-shadow: 0 4px 20px rgba(245, 245, 220, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
|
color: #1E1E1E;
|
|
font-weight: 700;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
box-shadow: 0 8px 25px rgba(245, 245, 220, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.leaderboard-container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 6rem 1rem 2rem;
|
|
min-height: calc(100vh - 200px);
|
|
}
|
|
|
|
.leaderboard-header {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.leaderboard-title {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.leaderboard-subtitle {
|
|
font-size: 1.1rem;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.disclaimer {
|
|
font-size: 0.9rem;
|
|
opacity: 0.8;
|
|
font-style: italic;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
.leaderboard-nav {
|
|
background: rgba(30, 30, 30, 0.6);
|
|
border-radius: 1rem;
|
|
padding: 1rem;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
margin-bottom: 2rem;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.leaderboard-tab {
|
|
padding: 0.75rem 1.5rem;
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border: 1px solid rgba(144, 238, 144, 0.2);
|
|
border-radius: 0.5rem;
|
|
color: #ffffff;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.leaderboard-tab:hover {
|
|
background: rgba(144, 238, 144, 0.1);
|
|
color: #90EE90;
|
|
border-color: #90EE90;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
|
}
|
|
|
|
.leaderboard-tab.active {
|
|
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(144, 238, 144, 0.05));
|
|
color: #90EE90;
|
|
border-color: #90EE90;
|
|
text-shadow: 0 0 15px rgba(144, 238, 144, 0.6);
|
|
}
|
|
|
|
.comparison-section {
|
|
background: rgba(30, 30, 30, 0.6);
|
|
border-radius: 1rem;
|
|
padding: 2rem;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: #90EE90;
|
|
margin-bottom: 1.5rem;
|
|
text-align: center;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
|
}
|
|
|
|
|
|
.comparison-type-selector {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.comparison-type-btn {
|
|
padding: 0.75rem 1.5rem;
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border: 1px solid rgba(144, 238, 144, 0.2);
|
|
border-radius: 0.5rem;
|
|
color: #ffffff;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.comparison-type-btn:hover {
|
|
background: rgba(144, 238, 144, 0.1);
|
|
color: #90EE90;
|
|
border-color: #90EE90;
|
|
}
|
|
|
|
.comparison-type-btn.active {
|
|
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(144, 238, 144, 0.05));
|
|
color: #90EE90;
|
|
border-color: #90EE90;
|
|
text-shadow: 0 0 15px rgba(144, 238, 144, 0.6);
|
|
}
|
|
|
|
.comparison-search {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.search-container {
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border-radius: 1rem;
|
|
padding: 1.5rem;
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
}
|
|
|
|
.search-container label {
|
|
display: block;
|
|
color: #90EE90;
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
margin-bottom: 0.5rem;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
|
}
|
|
|
|
.search-container input {
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border: 1px solid rgba(144, 238, 144, 0.2);
|
|
border-radius: 0.5rem;
|
|
color: #ffffff;
|
|
padding: 0.75rem 1rem;
|
|
font-size: 0.9rem;
|
|
width: 100%;
|
|
max-width: 400px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.search-container input:focus {
|
|
outline: none;
|
|
border-color: #90EE90;
|
|
box-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
|
}
|
|
|
|
.search-container input::placeholder {
|
|
color: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
/* Squadron tags + vehicle names echo skyquake glyphs (▄ ◢ ◊ ␗ etc.). */
|
|
#vehicleSearch, #squadronSearch {
|
|
font-family: 'skyquakesymbols', 'Inter', sans-serif;
|
|
}
|
|
|
|
.search-container {
|
|
position: relative;
|
|
}
|
|
|
|
.search-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: rgba(30, 30, 30, 0.95);
|
|
border: 1px solid rgba(144, 238, 144, 0.2);
|
|
border-top: none;
|
|
border-radius: 0 0 0.5rem 0.5rem;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
z-index: 1000;
|
|
display: none;
|
|
}
|
|
|
|
.search-results.show {
|
|
display: block;
|
|
}
|
|
|
|
.search-result-item {
|
|
padding: 0.75rem 1rem;
|
|
color: #ffffff;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.search-result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.search-result-item:hover,
|
|
.search-result-item.selected {
|
|
background: rgba(144, 238, 144, 0.1);
|
|
color: #90EE90;
|
|
}
|
|
|
|
.search-result-item .result-name {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.search-result-item .result-stats {
|
|
font-size: 0.8rem;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
margin-top: 0.2rem;
|
|
}
|
|
|
|
.selected-items {
|
|
margin-bottom: 2rem;
|
|
min-height: 60px;
|
|
}
|
|
|
|
.selected-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: rgba(144, 238, 144, 0.1);
|
|
border: 1px solid rgba(144, 238, 144, 0.2);
|
|
border-radius: 2rem;
|
|
padding: 0.5rem 1rem;
|
|
margin: 0.25rem;
|
|
color: #90EE90;
|
|
font-weight: 500;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.selected-item:hover {
|
|
background: rgba(144, 238, 144, 0.2);
|
|
border-color: #90EE90;
|
|
}
|
|
|
|
.selected-item-remove {
|
|
background: none;
|
|
border: none;
|
|
color: #ff4757;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.selected-item-remove:hover {
|
|
background: rgba(255, 71, 87, 0.2);
|
|
color: #ff3838;
|
|
}
|
|
|
|
.comparison-table-container {
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.comparison-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
min-width: 600px;
|
|
}
|
|
|
|
.comparison-table th {
|
|
background: linear-gradient(135deg, rgba(144, 238, 144, 0.15), rgba(144, 238, 144, 0.05));
|
|
color: #F5F5DC;
|
|
padding: 1rem 0.75rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
border-bottom: 1px solid rgba(144, 238, 144, 0.2);
|
|
font-size: 0.9rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.comparison-table th:first-child {
|
|
position: sticky;
|
|
left: 0;
|
|
background: linear-gradient(135deg, rgba(144, 238, 144, 0.2), rgba(144, 238, 144, 0.1));
|
|
z-index: 1;
|
|
}
|
|
|
|
.comparison-table td {
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid rgba(144, 238, 144, 0.1);
|
|
background: rgba(30, 30, 30, 0.4);
|
|
color: #ffffff;
|
|
transition: all 0.3s ease;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.comparison-table td:first-child {
|
|
text-align: left;
|
|
font-weight: 600;
|
|
position: sticky;
|
|
left: 0;
|
|
background: rgba(30, 30, 30, 0.6);
|
|
z-index: 1;
|
|
}
|
|
|
|
.comparison-table tr:hover td {
|
|
background: rgba(144, 238, 144, 0.05);
|
|
}
|
|
|
|
.comparison-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.stat-best {
|
|
color: #90EE90 !important;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.6);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.vs-divider {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #90EE90;
|
|
text-shadow: 0 0 15px rgba(144, 238, 144, 0.6);
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
.comparison-result {
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border-radius: 1rem;
|
|
padding: 2rem;
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
}
|
|
|
|
.comparison-cards {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 2rem;
|
|
align-items: start;
|
|
}
|
|
|
|
.comparison-card {
|
|
background: rgba(30, 30, 30, 0.6);
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
|
|
.comparison-name {
|
|
color: #90EE90;
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.3);
|
|
}
|
|
|
|
.comparison-stats {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.comparison-stat {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.comparison-stat:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.stat-label {
|
|
color: rgba(255, 255, 255, 0.8);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.stat-value {
|
|
color: #90EE90;
|
|
font-weight: 600;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.5);
|
|
}
|
|
|
|
.stat-value.better {
|
|
color: #90EE90;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.6);
|
|
}
|
|
|
|
.comparison-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
.winner-indicator {
|
|
background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%);
|
|
color: #1E1E1E;
|
|
padding: 0.75rem 1.5rem;
|
|
border-radius: 2rem;
|
|
font-weight: 700;
|
|
font-size: 0.9rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
box-shadow: 0 4px 15px rgba(245, 245, 220, 0.4);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { box-shadow: 0 4px 15px rgba(245, 245, 220, 0.4); }
|
|
50% { box-shadow: 0 6px 20px rgba(245, 245, 220, 0.6); }
|
|
100% { box-shadow: 0 4px 15px rgba(245, 245, 220, 0.4); }
|
|
}
|
|
|
|
.comparison-summary {
|
|
background: rgba(30, 30, 30, 0.4);
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
text-align: center;
|
|
}
|
|
|
|
.summary-text {
|
|
color: rgba(255, 255, 255, 0.9);
|
|
font-size: 0.95rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.comparison-placeholder {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
background: rgba(30, 30, 30, 0.8);
|
|
border-radius: 1rem;
|
|
border: 1px solid rgba(144, 238, 144, 0.1);
|
|
}
|
|
|
|
.loading-state {
|
|
text-align: center;
|
|
padding: 4rem;
|
|
color: #90EE90;
|
|
}
|
|
|
|
.loading-spinner {
|
|
border: 3px solid rgba(144, 238, 144, 0.3);
|
|
border-top: 3px solid #90EE90;
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 1rem;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Footer Styles */
|
|
footer {
|
|
background: rgba(30, 30, 30, 0.8);
|
|
backdrop-filter: blur(10px);
|
|
border-top: 1px solid rgba(144, 238, 144, 0.1);
|
|
padding: 2rem 0;
|
|
margin-top: auto;
|
|
}
|
|
|
|
footer a {
|
|
color: #888;
|
|
text-decoration: none;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
footer a:hover {
|
|
color: #90EE90;
|
|
text-shadow: 0 0 10px rgba(144, 238, 144, 0.5);
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body class="text-white antialiased">
|
|
<%- include('partials/nav', { activePage: 'leaderboards' }) %>
|
|
<div class="leaderboard-container">
|
|
<div class="leaderboard-header">
|
|
<h1 class="leaderboard-title"><%= t('leaderboard.comparisonTitle') %></h1>
|
|
<p class="leaderboard-subtitle"><%= t('leaderboard.comparisonSubtitle') %></p>
|
|
<div class="disclaimer">
|
|
<i class="fas fa-info-circle" style="margin-right: 0.5rem;"></i>
|
|
<%= t('leaderboard.comparisonHint') %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="leaderboard-nav">
|
|
<a href="/leaderboard/players" class="leaderboard-tab">
|
|
<i class="fas fa-user"></i> <%= t('common.players') %>
|
|
</a>
|
|
<a href="/leaderboard/vehicles" class="leaderboard-tab">
|
|
<i class="fas fa-fighter-jet"></i> <%= t('common.vehicles') %>
|
|
</a>
|
|
<a href="/leaderboard/squadrons" class="leaderboard-tab">
|
|
<i class="fas fa-users"></i> <%= t('common.squadrons') %>
|
|
</a>
|
|
<a href="/leaderboard/stats" class="leaderboard-tab">
|
|
<i class="fas fa-chart-bar"></i> <%= t('common.statistics') %>
|
|
</a>
|
|
<a href="/leaderboard/comparison" class="leaderboard-tab active">
|
|
<i class="fas fa-balance-scale"></i> <%= t('common.comparison') %>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="loading-state" id="loadingState">
|
|
<div class="loading-spinner"></div>
|
|
<p><%= t('leaderboard.loadingComparisonData') %></p>
|
|
</div>
|
|
|
|
<div id="comparisonContent" style="display: none;">
|
|
<div class="comparison-section">
|
|
<div class="comparison-type-selector">
|
|
<button class="comparison-type-btn active" onclick="switchComparisonType('players')" id="playersBtn">
|
|
<i class="fas fa-user"></i> <%= t('leaderboard.comparePlayers') %>
|
|
</button>
|
|
<button class="comparison-type-btn" onclick="switchComparisonType('squadrons')" id="squadronsBtn">
|
|
<i class="fas fa-users"></i> <%= t('leaderboard.compareSquadrons') %>
|
|
</button>
|
|
<button class="comparison-type-btn" onclick="switchComparisonType('vehicles')" id="vehiclesBtn">
|
|
<i class="fas fa-fighter-jet"></i> <%= t('leaderboard.compareVehicles') %>
|
|
</button>
|
|
<button class="comparison-type-btn" onclick="switchComparisonType('player-vehicles')" id="playerVehiclesBtn">
|
|
<i class="fas fa-user-cog"></i> <%= t('leaderboard.playersAndVehicles') %>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-section" id="playerComparison">
|
|
<h2 class="section-title"><%= t('leaderboard.playerComparison') %></h2>
|
|
|
|
<div class="comparison-search">
|
|
<div class="search-container">
|
|
<label for="playerSearch"><%= t('leaderboard.addPlayersToCompare') %></label>
|
|
<input type="text" id="playerSearch" placeholder="<%= t('leaderboard.searchSelectPlayers') %>" oninput="searchPlayers()" onkeydown="handlePlayerKeydown(event)">
|
|
<div class="search-results" id="playerResults"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="selected-items" id="selectedPlayers">
|
|
</div>
|
|
|
|
<div class="comparison-result" id="playerComparisonResult" style="display: none;">
|
|
<div class="comparison-table-container">
|
|
<table class="comparison-table" id="playerComparisonTable">
|
|
<thead id="playerTableHead">
|
|
</thead>
|
|
<tbody id="playerTableBody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-placeholder" id="playerComparisonPlaceholder">
|
|
<i class="fas fa-users" style="font-size: 3rem; color: rgba(57, 255, 20, 0.3); margin-bottom: 1rem;"></i>
|
|
<p style="color: rgba(255, 255, 255, 0.6);"><%= t('leaderboard.selectPlayersToCompare') %></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-section" id="squadronComparison" style="display: none;">
|
|
<h2 class="section-title"><%= t('leaderboard.squadronComparison') %></h2>
|
|
|
|
<div class="comparison-search">
|
|
<div class="search-container" id="squadronSearchContainer">
|
|
<label for="squadronSearch"><%= t('leaderboard.addSquadronsToCompare') %></label>
|
|
<input type="text" id="squadronSearch" placeholder="<%= t('leaderboard.typeSquadronName') %>" autocomplete="off" oninput="searchSquadronsDropdown()" onkeydown="handleSquadronKeydown(event)">
|
|
<div class="search-results" id="squadronDropdown" style="display:none; max-height:300px; overflow-y:auto;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="selected-items" id="selectedSquadrons">
|
|
</div>
|
|
|
|
<div class="comparison-result" id="squadronComparisonResult" style="display: none;">
|
|
<div class="comparison-table-container">
|
|
<table class="comparison-table" id="squadronComparisonTable">
|
|
<thead id="squadronTableHead">
|
|
</thead>
|
|
<tbody id="squadronTableBody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-placeholder" id="squadronComparisonPlaceholder">
|
|
<i class="fas fa-users" style="font-size: 3rem; color: rgba(57, 255, 20, 0.3); margin-bottom: 1rem;"></i>
|
|
<p style="color: rgba(255, 255, 255, 0.6);"><%= t('leaderboard.selectSquadronsToCompare') %></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-section" id="vehicleComparison" style="display: none;">
|
|
<h2 class="section-title"><%= t('leaderboard.vehicleComparison') %></h2>
|
|
|
|
<div class="comparison-search">
|
|
<div class="search-container">
|
|
<label for="vehicleSearch"><%= t('leaderboard.addVehiclesToCompare') %></label>
|
|
<input type="text" id="vehicleSearch" placeholder="<%= t('leaderboard.searchSelectVehicles') %>" oninput="searchVehicles()" onkeydown="handleVehicleKeydown(event)">
|
|
<div class="search-results" id="vehicleResults"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="selected-items" id="selectedVehicles">
|
|
</div>
|
|
|
|
<div class="comparison-result" id="vehicleComparisonResult" style="display: none;">
|
|
<div class="comparison-table-container">
|
|
<table class="comparison-table" id="vehicleComparisonTable">
|
|
<thead id="vehicleTableHead">
|
|
</thead>
|
|
<tbody id="vehicleTableBody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-placeholder" id="vehicleComparisonPlaceholder">
|
|
<i class="fas fa-fighter-jet" style="font-size: 3rem; color: rgba(57, 255, 20, 0.3); margin-bottom: 1rem;"></i>
|
|
<p style="color: rgba(255, 255, 255, 0.6);"><%= t('leaderboard.selectVehiclesToCompare') %></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-section" id="playerVehicleComparison" style="display: none;">
|
|
<h2 class="section-title"><%= t('leaderboard.playersVehiclesComparison') %></h2>
|
|
|
|
<div class="comparison-search">
|
|
<div class="search-container">
|
|
<label for="playerVehicleComboSearch"><%= t('leaderboard.addPlayerVehicleCombos') %></label>
|
|
<input type="text" id="playerVehicleComboSearch" placeholder="<%= t('leaderboard.searchForPlayers') %>" oninput="searchPlayersForCombo()" onkeydown="handlePlayerVehicleComboKeydown(event)">
|
|
<div class="search-results" id="playerVehicleComboResults"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="selected-items" id="selectedPlayerVehicleCombos">
|
|
</div>
|
|
|
|
<div class="comparison-result" id="playerVehicleComparisonResult" style="display: none;">
|
|
<div class="comparison-table-container">
|
|
<table class="comparison-table" id="playerVehicleComparisonTable">
|
|
<thead id="playerVehicleTableHead">
|
|
</thead>
|
|
<tbody id="playerVehicleTableBody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="comparison-placeholder" id="playerVehicleComparisonPlaceholder">
|
|
<i class="fas fa-user-cog" style="font-size: 3rem; color: rgba(57, 255, 20, 0.3); margin-bottom: 1rem;"></i>
|
|
<p style="color: rgba(255, 255, 255, 0.6);"><%= t('leaderboard.selectPlayersVehiclesToCompare') %></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<%- include('partials/footer') %>
|
|
|
|
<script>
|
|
window.__lang = '<%= lang %>';
|
|
window.__i18n = <%- localeJson %>;
|
|
window.__t = function(key) {
|
|
var parts = key.split('.'), obj = window.__i18n;
|
|
for (var i = 0; i < parts.length; i++) { obj = obj && obj[parts[i]]; }
|
|
return obj !== undefined ? obj : key;
|
|
};
|
|
window.switchLanguage = function(lang) {
|
|
var next = lang || (document.documentElement.lang === 'en' ? 'ru' : 'en');
|
|
if (next === document.documentElement.lang) return;
|
|
document.cookie = 'lang=' + next + ';path=/;max-age=31536000;SameSite=Lax';
|
|
window.location.reload();
|
|
};
|
|
</script>
|
|
<script src="/js/main.js?v=3"></script>
|
|
<script src="/js/api-client.js?v=2"></script>
|
|
<script src="/js/vehicle-i18n.js"></script>
|
|
<script src="/js/seasons-filter.js"></script>
|
|
<script>
|
|
let playersData = [];
|
|
let squadronsData = [];
|
|
let currentComparisonType = 'players';
|
|
let selectedPlayers = [];
|
|
let selectedVehicles = [];
|
|
let selectedSquadrons = [];
|
|
const playerVehicleCache = new Map();
|
|
const vehicleAggregateCache = new Map();
|
|
let comparisonVehicleList = [];
|
|
|
|
async function loadComparisonData() {
|
|
const loadingState = document.getElementById('loadingState');
|
|
const comparisonContent = document.getElementById('comparisonContent');
|
|
|
|
try {
|
|
await window.ensureAPIClient();
|
|
|
|
const [playersResponse, squadronsResponse] = await Promise.all([
|
|
window.apiClient.getPlayerLeaderboard(),
|
|
window.apiClient.getSquadronLeaderboard()
|
|
]);
|
|
await ensureComparisonVehicleList();
|
|
|
|
playersData = playersResponse.players || [];
|
|
squadronsData = squadronsResponse.squadrons || [];
|
|
|
|
playersData = playersData.map(player => {
|
|
const total_kills = player.total_kills || ((player.ground_kills || 0) + (player.air_kills || 0));
|
|
const assists = player.total_assists || player.assists || 0;
|
|
const captures = player.total_captures || player.captures || 0;
|
|
const wins = player.wins || 0;
|
|
const battles = player.total_battles || 0;
|
|
const winRate = battles > 0 ? (wins / battles) * 100 : 0;
|
|
const kdr = (player.deaths || 0) > 0 ? (total_kills / player.deaths) : total_kills;
|
|
const kps = battles > 0 ? (total_kills / battles) : 0;
|
|
|
|
return {
|
|
...player,
|
|
total_kills,
|
|
assists,
|
|
captures,
|
|
wins,
|
|
battles,
|
|
winRate,
|
|
kdr,
|
|
kps
|
|
};
|
|
});
|
|
|
|
loadingState.style.display = 'none';
|
|
comparisonContent.style.display = 'block';
|
|
|
|
} catch (error) {
|
|
console.error('Error loading comparison data:', error);
|
|
loadingState.innerHTML = `
|
|
<div style="color: #ff3838;">
|
|
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; margin-bottom: 1rem;"></i>
|
|
<h3>${__t('leaderboard.failedToLoadComparison')}</h3>
|
|
<p>${__t('leaderboard.pleaseRefresh')}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function switchComparisonType(type) {
|
|
currentComparisonType = type;
|
|
document.getElementById('playersBtn').classList.toggle('active', type === 'players');
|
|
document.getElementById('squadronsBtn').classList.toggle('active', type === 'squadrons');
|
|
document.getElementById('vehiclesBtn').classList.toggle('active', type === 'vehicles');
|
|
document.getElementById('playerVehiclesBtn').classList.toggle('active', type === 'player-vehicles');
|
|
|
|
document.getElementById('playerComparison').style.display = type === 'players' ? 'block' : 'none';
|
|
document.getElementById('squadronComparison').style.display = type === 'squadrons' ? 'block' : 'none';
|
|
document.getElementById('vehicleComparison').style.display = type === 'vehicles' ? 'block' : 'none';
|
|
document.getElementById('playerVehicleComparison').style.display = type === 'player-vehicles' ? 'block' : 'none';
|
|
|
|
document.getElementById('playerSearch').value = '';
|
|
if (document.getElementById('squadronSearch')) {
|
|
document.getElementById('squadronSearch').value = '';
|
|
}
|
|
document.getElementById('vehicleSearch').value = '';
|
|
if (document.getElementById('playerVehicleComboSearch')) {
|
|
document.getElementById('playerVehicleComboSearch').value = '';
|
|
}
|
|
hideSearchResults();
|
|
}
|
|
|
|
function hideSearchResults() {
|
|
document.getElementById('playerResults').classList.remove('show');
|
|
const sqDropdown = document.getElementById('squadronDropdown');
|
|
if (sqDropdown) sqDropdown.style.display = 'none';
|
|
document.getElementById('vehicleResults').classList.remove('show');
|
|
if (document.getElementById('playerVehicleComboResults')) {
|
|
document.getElementById('playerVehicleComboResults').classList.remove('show');
|
|
}
|
|
}
|
|
|
|
async function ensureComparisonVehicleList() {
|
|
if (comparisonVehicleList.length) return comparisonVehicleList;
|
|
const data = await window.apiClient.request('/api/analytics/vehicle-list');
|
|
comparisonVehicleList = (data && data.vehicles) || [];
|
|
return comparisonVehicleList;
|
|
}
|
|
|
|
function vehicleDisplay(internal, fallback) {
|
|
if (!internal) return fallback || '';
|
|
if (window.vehicleI18n && window.vehicleI18n.translate) {
|
|
return window.vehicleI18n.translate(internal, fallback || internal);
|
|
}
|
|
return fallback || internal;
|
|
}
|
|
|
|
async function fetchVehicleComparisonAggregate(internal) {
|
|
const key = String(internal || '').toLowerCase();
|
|
if (!key) return null;
|
|
if (vehicleAggregateCache.has(key)) return vehicleAggregateCache.get(key);
|
|
|
|
const rows = [];
|
|
let page = 1;
|
|
let totalPages = 1;
|
|
|
|
do {
|
|
const response = await window.apiClient.request(`/api/leaderboard/vehicles?vehicle_internal=${encodeURIComponent(key)}&limit=100&page=${page}`);
|
|
const batch = (response && response.vehicles) || [];
|
|
rows.push(...batch);
|
|
totalPages = Math.max(1, Number(response?.pagination?.total_pages) || 1);
|
|
page += 1;
|
|
} while (page <= totalPages);
|
|
|
|
const aggregate = rows.reduce((acc, row) => {
|
|
acc.total_battles += Number(row.battles) || 0;
|
|
acc.total_wins += Number(row.wins) || 0;
|
|
acc.ground_kills += Number(row.ground_kills) || 0;
|
|
acc.air_kills += Number(row.air_kills) || 0;
|
|
acc.total_kills += Number(row.total_kills) || 0;
|
|
acc.players += 1;
|
|
return acc;
|
|
}, {
|
|
vehicle_internal: key,
|
|
vehicle: vehicleDisplay(key, rows[0]?.vehicle || key),
|
|
total_battles: 0,
|
|
total_wins: 0,
|
|
ground_kills: 0,
|
|
air_kills: 0,
|
|
total_kills: 0,
|
|
players: 0
|
|
});
|
|
|
|
aggregate.avg_win_rate = aggregate.total_battles > 0 ? ((aggregate.total_wins / aggregate.total_battles) * 100) : 0;
|
|
aggregate.avg_kills = aggregate.players > 0 ? (aggregate.total_kills / aggregate.players) : 0;
|
|
|
|
vehicleAggregateCache.set(key, aggregate);
|
|
return aggregate;
|
|
}
|
|
|
|
let sqSearchTimeout;
|
|
function searchSquadronsDropdown() {
|
|
clearTimeout(sqSearchTimeout);
|
|
const input = document.getElementById('squadronSearch');
|
|
const dropdown = document.getElementById('squadronDropdown');
|
|
const val = input.value.trim().toLowerCase();
|
|
|
|
if (val.length < 1) {
|
|
dropdown.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
sqSearchTimeout = setTimeout(() => {
|
|
const hits = squadronsData
|
|
.filter(s => s.tag_name.toLowerCase().includes(val) || s.long_name.toLowerCase().includes(val))
|
|
.slice(0, 12)
|
|
.sort((a, b) => (b.points?.total_points || b.total_kills) - (a.points?.total_points || a.total_kills));
|
|
|
|
if (!hits.length) {
|
|
dropdown.innerHTML = '<div class="search-result-item" style="color:rgba(255,255,255,0.5);">' + __t('common.noSquadronsFound') + '</div>';
|
|
} else {
|
|
dropdown.innerHTML = hits.map(s =>
|
|
`<div class="search-result-item" onclick="addSquadron('${escapeHtml(s.tag_name)}')">
|
|
<span style="font-family:'skyquakesymbols','Inter',sans-serif; color:#90EE90; font-size:0.95rem;">${escapeHtml(s.tag_name)}</span>
|
|
<span style="color:rgba(255,255,255,0.35); font-size:0.85rem; margin-left:0.5rem;">${escapeHtml(s.long_name)}</span>
|
|
<div class="result-stats">${s.player_count} ${__t('common.membersCount')} • ${s.total_kills.toLocaleString()} ${__t('js.killsSuffix')} • ${s.win_rate.toFixed(1)}% ${__t('js.winRateSuffix')}</div>
|
|
</div>`
|
|
).join('');
|
|
}
|
|
dropdown.style.display = 'block';
|
|
}, 100);
|
|
}
|
|
|
|
let selectedPlayerVehicleCombos = [];
|
|
let currentPlayerForVehicleSelection = null;
|
|
|
|
function searchPlayersForCombo() {
|
|
const searchTerm = document.getElementById('playerVehicleComboSearch').value.toLowerCase().trim();
|
|
const resultsDiv = document.getElementById('playerVehicleComboResults');
|
|
|
|
if (searchTerm.length < 2) {
|
|
resultsDiv.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
const filteredPlayers = playersData
|
|
.filter(player => player.nick.toLowerCase().includes(searchTerm))
|
|
.slice(0, 10)
|
|
.sort((a, b) => b.total_kills - a.total_kills);
|
|
|
|
if (filteredPlayers.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="search-result-item">' + __t('common.noPlayersFound') + '</div>';
|
|
} else {
|
|
resultsDiv.innerHTML = filteredPlayers.map(player => `
|
|
<div class="search-result-item" onclick="selectPlayerForCombo(${player.uid})">
|
|
<div class="result-name">${escapeHtml(player.nick)}</div>
|
|
<div class="result-stats">${formatNumber(player.total_kills)} ${__t('js.killsSuffix')} • ${player.winRate.toFixed(1)}% ${__t('js.winRateSuffix')}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
resultsDiv.classList.add('show');
|
|
}
|
|
|
|
async function selectPlayerForCombo(uid) {
|
|
const player = playersData.find(p => p.uid == uid);
|
|
if (!player) return;
|
|
|
|
currentPlayerForVehicleSelection = player;
|
|
|
|
document.getElementById('playerVehicleComboSearch').value = '';
|
|
hideSearchResults();
|
|
|
|
const resultsDiv = document.getElementById('playerVehicleComboResults');
|
|
resultsDiv.innerHTML = '<div class="search-result-item">' + __t('common.loading') + '</div>';
|
|
resultsDiv.classList.add('show');
|
|
|
|
try {
|
|
const playerVehicles = await getVehiclesForPlayer(player.uid);
|
|
if (currentPlayerForVehicleSelection?.uid != player.uid) return;
|
|
showVehicleSelectionForPlayer(player, playerVehicles);
|
|
} catch (error) {
|
|
console.error('Failed to load player vehicles for comparison:', error);
|
|
resultsDiv.innerHTML = '<div class="search-result-item">' + __t('leaderboard.noVehiclesForPlayer') + '</div>';
|
|
resultsDiv.classList.add('show');
|
|
}
|
|
}
|
|
|
|
async function getVehiclesForPlayer(uid) {
|
|
const cacheKey = String(uid);
|
|
if (playerVehicleCache.has(cacheKey)) return playerVehicleCache.get(cacheKey);
|
|
|
|
const playerResponse = await window.apiClient.request('/api/player/' + encodeURIComponent(uid));
|
|
const playerVehicles = (playerResponse.vehicles || [])
|
|
.filter(v => v && v.vehicle && v.vehicle !== 'DISCONNECTED')
|
|
.map(v => {
|
|
const stats = v.stats || {};
|
|
const groundKills = Number(stats.ground_kills) || 0;
|
|
const airKills = Number(stats.air_kills) || 0;
|
|
const battles = Number(stats.total_battles) || 0;
|
|
const wins = Number(stats.wins) || 0;
|
|
const assists = Number(stats.assists) || 0;
|
|
const deaths = Number(stats.deaths) || 0;
|
|
const captures = Number(stats.captures) || 0;
|
|
const vehicleInternal = (v.vehicle_internal || '').toLowerCase();
|
|
const displayName = vehicleDisplay(vehicleInternal, v.vehicle);
|
|
return {
|
|
vehicle_internal: vehicleInternal,
|
|
vehicle: displayName,
|
|
stats: {
|
|
ground_kills: groundKills,
|
|
air_kills: airKills,
|
|
total_battles: battles,
|
|
wins,
|
|
assists,
|
|
deaths,
|
|
captures
|
|
},
|
|
total_kills: groundKills + airKills,
|
|
battles,
|
|
wins,
|
|
ground_kills: groundKills,
|
|
air_kills: airKills,
|
|
assists,
|
|
deaths,
|
|
captures
|
|
};
|
|
})
|
|
.sort((a, b) => b.total_kills - a.total_kills);
|
|
|
|
playerVehicleCache.set(cacheKey, playerVehicles);
|
|
return playerVehicles;
|
|
}
|
|
|
|
function showVehicleSelectionForPlayer(player, playerVehicles) {
|
|
const resultsDiv = document.getElementById('playerVehicleComboResults');
|
|
|
|
if (playerVehicles.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="search-result-item">' + __t('leaderboard.noVehiclesForPlayer') + '</div>';
|
|
resultsDiv.classList.add('show');
|
|
return;
|
|
}
|
|
|
|
resultsDiv.innerHTML = `
|
|
<div style="padding: 1rem; background: rgba(57, 255, 20, 0.1); border-bottom: 1px solid rgba(57, 255, 20, 0.3);">
|
|
<div style="font-weight: 600; color: #39ff14; margin-bottom: 0.75rem;">
|
|
${__t('leaderboard.selectVehicleFor')} ${escapeHtml(player.nick)}:
|
|
</div>
|
|
<select id="vehicleDropdown" onchange="handleVehicleSelection(${player.uid})" style="width: 100%; background: rgba(15, 15, 26, 0.8); border: 1px solid rgba(57, 255, 20, 0.3); border-radius: 0.5rem; color: #ffffff; padding: 0.75rem; font-size: 0.95rem;">
|
|
<option value="">${__t('leaderboard.selectAVehicle')}</option>
|
|
${playerVehicles.map(vehicleEntry => `
|
|
<option value="${escapeHtml(vehicleEntry.vehicle_internal || vehicleEntry.vehicle)}" data-stats="${escapeHtml(encodeURIComponent(JSON.stringify(vehicleEntry)))}">
|
|
${escapeHtml(vehicleEntry.vehicle)} (${vehicleEntry.total_kills} kills, ${vehicleEntry.battles} battles)
|
|
</option>
|
|
`).join('')}
|
|
</select>
|
|
</div>
|
|
`;
|
|
|
|
resultsDiv.classList.add('show');
|
|
}
|
|
|
|
function handleVehicleSelection(uid) {
|
|
const dropdown = document.getElementById('vehicleDropdown');
|
|
const selectedOption = dropdown.options[dropdown.selectedIndex];
|
|
|
|
if (!selectedOption.value) return;
|
|
|
|
const vehicleInternal = selectedOption.value;
|
|
const vehicleStats = JSON.parse(decodeURIComponent(selectedOption.getAttribute('data-stats')));
|
|
|
|
addPlayerVehicleComboWithStats(uid, vehicleInternal, vehicleStats);
|
|
}
|
|
|
|
function addPlayerVehicleComboWithStats(uid, vehicleInternal, vehicleStats) {
|
|
const player = playersData.find(p => p.uid == uid);
|
|
|
|
if (!player || !vehicleStats) return;
|
|
|
|
const comboKey = `${uid}-${vehicleInternal || vehicleStats.vehicle}`;
|
|
if (selectedPlayerVehicleCombos.some(combo => combo.key === comboKey)) return;
|
|
|
|
selectedPlayerVehicleCombos.push({
|
|
key: comboKey,
|
|
player: player,
|
|
vehicleData: vehicleStats,
|
|
displayName: `${player.nick} (${vehicleStats.vehicle || vehicleInternal})`
|
|
});
|
|
|
|
updateSelectedPlayerVehicleCombos();
|
|
updatePlayerVehicleComparison();
|
|
hideSearchResults();
|
|
}
|
|
|
|
function removePlayerVehicleCombo(comboKey) {
|
|
selectedPlayerVehicleCombos = selectedPlayerVehicleCombos.filter(combo => combo.key !== comboKey);
|
|
updateSelectedPlayerVehicleCombos();
|
|
updatePlayerVehicleComparison();
|
|
}
|
|
|
|
function updateSelectedPlayerVehicleCombos() {
|
|
const container = document.getElementById('selectedPlayerVehicleCombos');
|
|
if (selectedPlayerVehicleCombos.length === 0) {
|
|
container.innerHTML = '<p style="color: rgba(255, 255, 255, 0.6); text-align: center; padding: 1rem;">' + __t('leaderboard.noPlayerVehicleSelected') + '</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = selectedPlayerVehicleCombos.map(combo => `
|
|
<span class="selected-item">
|
|
${escapeHtml(combo.displayName)}
|
|
<button class="selected-item-remove" data-combo-key="${combo.key}" data-type="player-vehicle-combo">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</span>
|
|
`).join('');
|
|
}
|
|
|
|
function updatePlayerVehicleComparison() {
|
|
const resultDiv = document.getElementById('playerVehicleComparisonResult');
|
|
const placeholderDiv = document.getElementById('playerVehicleComparisonPlaceholder');
|
|
|
|
if (selectedPlayerVehicleCombos.length < 2) {
|
|
resultDiv.style.display = 'none';
|
|
placeholderDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
displayPlayerVehicleComparisonTable();
|
|
resultDiv.style.display = 'block';
|
|
placeholderDiv.style.display = 'none';
|
|
}
|
|
|
|
function displayPlayerVehicleComparisonTable() {
|
|
const stats = [
|
|
{ label: __t('common.battles'), key: 'battles', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.wins'), key: 'wins', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.winRate'), key: 'win_rate', format: (val) => val.toFixed(1) + '%', higher: true },
|
|
{ label: __t('common.totalKills'), key: 'total_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.groundKills'), key: 'ground_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.airKills'), key: 'air_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('leaderboard.kdRatio'), key: 'kd_ratio', format: (val) => val.toFixed(2), higher: true },
|
|
{ label: __t('common.assists'), key: 'assists', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.captures'), key: 'captures', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.deaths'), key: 'deaths', format: (val) => formatNumber(val), higher: false }
|
|
];
|
|
|
|
const headerRow = `
|
|
<tr>
|
|
<th>${__t('leaderboard.statistic')}</th>
|
|
${selectedPlayerVehicleCombos.map(combo => `<th>${escapeHtml(combo.displayName)}</th>`).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
const bodyRows = stats.map(stat => {
|
|
const values = selectedPlayerVehicleCombos.map(combo => {
|
|
const vehicleData = combo.vehicleData;
|
|
let value = vehicleData[stat.key] || 0;
|
|
|
|
if (stat.key === 'win_rate') {
|
|
value = vehicleData.battles > 0 ? ((vehicleData.wins || 0) / vehicleData.battles) * 100 : 0;
|
|
} else if (stat.key === 'kd_ratio') {
|
|
value = (vehicleData.deaths || 0) > 0 ? (vehicleData.total_kills || 0) / vehicleData.deaths : (vehicleData.total_kills || 0);
|
|
}
|
|
|
|
return value;
|
|
});
|
|
|
|
const bestValue = stat.higher ? Math.max(...values) : Math.min(...values);
|
|
|
|
const cells = selectedPlayerVehicleCombos.map((combo, index) => {
|
|
const value = values[index];
|
|
const isBest = value === bestValue && selectedPlayerVehicleCombos.length > 1;
|
|
return `<td class="${isBest ? 'stat-best' : ''}">${stat.format(value)}</td>`;
|
|
}).join('');
|
|
|
|
return `<tr><td>${stat.label}</td>${cells}</tr>`;
|
|
}).join('');
|
|
|
|
document.getElementById('playerVehicleTableHead').innerHTML = headerRow;
|
|
document.getElementById('playerVehicleTableBody').innerHTML = bodyRows;
|
|
}
|
|
|
|
function handlePlayerVehicleComboKeydown(event) {
|
|
if (event.key === 'Escape') {
|
|
hideSearchResults();
|
|
document.getElementById('playerVehicleComboSearch').blur();
|
|
}
|
|
}
|
|
|
|
function searchPlayers() {
|
|
const searchTerm = document.getElementById('playerSearch').value.toLowerCase().trim();
|
|
const resultsDiv = document.getElementById('playerResults');
|
|
|
|
if (searchTerm.length < 2) {
|
|
resultsDiv.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
const filteredPlayers = playersData
|
|
.filter(player => player.nick.toLowerCase().includes(searchTerm))
|
|
.slice(0, 10)
|
|
.sort((a, b) => b.total_kills - a.total_kills);
|
|
|
|
if (filteredPlayers.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="search-result-item">' + __t('common.noPlayersFound') + '</div>';
|
|
} else {
|
|
resultsDiv.innerHTML = filteredPlayers.map(player => {
|
|
const squadronTag = player.squadron_name ? `<span class="player-squadron" style="font-family: 'skyquakesymbols', 'Inter', sans-serif !important; color: rgba(255, 255, 255, 0.4); font-size: 0.85rem; margin-right: 0.4rem;">${escapeHtml(player.squadron_name)}</span>` : '';
|
|
return `
|
|
<div class="search-result-item" onclick="addPlayer(${player.uid})">
|
|
<div class="result-name">${squadronTag}${escapeHtml(player.nick)}</div>
|
|
<div class="result-stats">${player.total_kills} ${__t('js.killsSuffix')} • ${player.winRate.toFixed(1)}% ${__t('js.winRateSuffix')}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
resultsDiv.classList.add('show');
|
|
}
|
|
|
|
async function searchVehicles() {
|
|
const searchTerm = document.getElementById('vehicleSearch').value.toLowerCase().trim();
|
|
const resultsDiv = document.getElementById('vehicleResults');
|
|
|
|
if (searchTerm.length < 1) {
|
|
resultsDiv.classList.remove('show');
|
|
return;
|
|
}
|
|
|
|
await ensureComparisonVehicleList();
|
|
if (window.vehicleI18n && window.vehicleI18n.ensureLoaded) await window.vehicleI18n.ensureLoaded();
|
|
|
|
const filteredVehicles = [];
|
|
for (const entry of comparisonVehicleList) {
|
|
const display = vehicleDisplay(entry.vehicle_internal, entry.vehicle_internal);
|
|
if (!display.replace(/\u00A0/g, ' ').toLowerCase().includes(searchTerm)) continue;
|
|
filteredVehicles.push({
|
|
vehicle: display,
|
|
vehicle_internal: entry.vehicle_internal,
|
|
total: entry.total || 0
|
|
});
|
|
if (filteredVehicles.length >= 25) break;
|
|
}
|
|
|
|
if (filteredVehicles.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="search-result-item">' + __t('common.noVehiclesFound') + '</div>';
|
|
} else {
|
|
resultsDiv.innerHTML = filteredVehicles.map(vehicle => `
|
|
<div class="search-result-item" onclick="addVehicle('${escapeHtml(vehicle.vehicle_internal)}')">
|
|
<div class="result-name" data-vehicle-internal="${escapeHtml(vehicle.vehicle_internal || '')}" style="font-family:'skyquakesymbols','Inter',sans-serif;">${escapeHtml(vehicle.vehicle)}</div>
|
|
<div class="result-stats">${vehicle.total} ${__t('analytics.compColSpawns').toLowerCase()}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
resultsDiv.classList.add('show');
|
|
}
|
|
|
|
|
|
function addPlayer(uid) {
|
|
const player = playersData.find(p => p.uid == uid);
|
|
if (!player || selectedPlayers.some(p => p.uid === player.uid)) return;
|
|
|
|
selectedPlayers.push(player);
|
|
updateSelectedPlayers();
|
|
updatePlayerComparison();
|
|
document.getElementById('playerSearch').value = '';
|
|
hideSearchResults();
|
|
}
|
|
|
|
async function addVehicle(vehicleInternal) {
|
|
const key = String(vehicleInternal || '').toLowerCase();
|
|
if (!key || selectedVehicles.some(v => (v.vehicle_internal || '').toLowerCase() === key)) return;
|
|
|
|
const vehicle = await fetchVehicleComparisonAggregate(key);
|
|
if (!vehicle) return;
|
|
|
|
selectedVehicles.push(vehicle);
|
|
updateSelectedVehicles();
|
|
updateVehicleComparison();
|
|
document.getElementById('vehicleSearch').value = '';
|
|
hideSearchResults();
|
|
}
|
|
|
|
function removePlayer(uid) {
|
|
selectedPlayers = selectedPlayers.filter(p => p.uid != uid); // Use loose equality to handle string/number conversion
|
|
updateSelectedPlayers();
|
|
updatePlayerComparison();
|
|
}
|
|
|
|
function removeVehicle(vehicleInternal) {
|
|
const key = String(vehicleInternal || '').toLowerCase();
|
|
selectedVehicles = selectedVehicles.filter(v => (v.vehicle_internal || '').toLowerCase() !== key);
|
|
updateSelectedVehicles();
|
|
updateVehicleComparison();
|
|
}
|
|
|
|
function addSquadron(squadronName) {
|
|
const squadron = squadronsData.find(s => s.tag_name === squadronName);
|
|
if (!squadron || selectedSquadrons.some(s => s.tag_name === squadron.tag_name)) return;
|
|
|
|
selectedSquadrons.push(squadron);
|
|
updateSelectedSquadrons();
|
|
updateSquadronComparison();
|
|
document.getElementById('squadronSearch').value = '';
|
|
document.getElementById('squadronDropdown').style.display = 'none';
|
|
}
|
|
|
|
function removeSquadron(squadronName) {
|
|
selectedSquadrons = selectedSquadrons.filter(s => s.tag_name !== squadronName);
|
|
updateSelectedSquadrons();
|
|
updateSquadronComparison();
|
|
}
|
|
|
|
function updateSelectedPlayers() {
|
|
const container = document.getElementById('selectedPlayers');
|
|
if (selectedPlayers.length === 0) {
|
|
container.innerHTML = '<p style="color: rgba(255, 255, 255, 0.6); text-align: center; padding: 1rem;">' + __t('leaderboard.noPlayersSelected') + '</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = selectedPlayers.map((player, index) => `
|
|
<span class="selected-item">
|
|
${escapeHtml(player.nick)}
|
|
<button class="selected-item-remove" data-player-uid="${player.uid}" data-type="player">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</span>
|
|
`).join('');
|
|
}
|
|
|
|
function updateSelectedVehicles() {
|
|
const container = document.getElementById('selectedVehicles');
|
|
if (selectedVehicles.length === 0) {
|
|
container.innerHTML = '<p style="color: rgba(255, 255, 255, 0.6); text-align: center; padding: 1rem;">' + __t('leaderboard.noVehiclesSelected') + '</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = selectedVehicles.map((vehicle, index) => `
|
|
<span class="selected-item" data-vehicle-internal="${escapeHtml(vehicle.vehicle_internal || '')}" style="font-family:'skyquakesymbols','Inter',sans-serif;">
|
|
${escapeHtml(vehicle.vehicle)}
|
|
<button class="selected-item-remove" data-vehicle-internal="${escapeHtml(vehicle.vehicle_internal || '')}" data-type="vehicle">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</span>
|
|
`).join('');
|
|
}
|
|
|
|
function updateSelectedSquadrons() {
|
|
const container = document.getElementById('selectedSquadrons');
|
|
if (selectedSquadrons.length === 0) {
|
|
container.innerHTML = '<p style="color: rgba(255, 255, 255, 0.6); text-align: center; padding: 1rem;">' + __t('leaderboard.noSquadronsSelected') + '</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = selectedSquadrons.map((squadron, index) => `
|
|
<span class="selected-item">
|
|
<span style="font-family:'skyquakesymbols','Inter',sans-serif;">${escapeHtml(squadron.tag_name)}</span>
|
|
<button class="selected-item-remove" data-squadron-name="${escapeHtml(squadron.tag_name)}" data-type="squadron">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</span>
|
|
`).join('');
|
|
}
|
|
|
|
function updatePlayerComparison() {
|
|
const resultDiv = document.getElementById('playerComparisonResult');
|
|
const placeholderDiv = document.getElementById('playerComparisonPlaceholder');
|
|
|
|
if (selectedPlayers.length < 2) {
|
|
resultDiv.style.display = 'none';
|
|
placeholderDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
displayPlayerComparisonTable();
|
|
resultDiv.style.display = 'block';
|
|
placeholderDiv.style.display = 'none';
|
|
}
|
|
|
|
function displayPlayerComparisonTable() {
|
|
const stats = [
|
|
{ label: __t('common.totalKills'), key: 'total_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('leaderboard.kdRatio'), key: 'kdr', format: (val) => val.toFixed(2), higher: true },
|
|
{ label: __t('common.winRate'), key: 'winRate', format: (val) => val.toFixed(1) + '%', higher: true },
|
|
{ label: __t('leaderboard.killsPerSpawnShort'), key: 'kps', format: (val) => val.toFixed(2), higher: true },
|
|
{ label: __t('common.battles'), key: 'battles', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.wins'), key: 'wins', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.assists'), key: 'assists', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.captures'), key: 'captures', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.deaths'), key: 'deaths', format: (val) => formatNumber(val), higher: false }
|
|
];
|
|
|
|
// Create table header
|
|
const headerRow = `
|
|
<tr>
|
|
<th>${__t('leaderboard.statistic')}</th>
|
|
${selectedPlayers.map(player => {
|
|
const squadronTag = player.squadron_name ? `<span class="player-squadron" style="font-family: 'skyquakesymbols', 'Inter', sans-serif !important; color: rgba(255, 255, 255, 0.4); font-size: 0.75rem; margin-right: 0.4rem;">${escapeHtml(player.squadron_name)}</span>` : '';
|
|
return `<th>${squadronTag}${escapeHtml(player.nick)}</th>`;
|
|
}).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
// Create table body
|
|
const bodyRows = stats.map(stat => {
|
|
const values = selectedPlayers.map(player => player[stat.key] || 0);
|
|
const bestValue = stat.higher ? Math.max(...values) : Math.min(...values);
|
|
|
|
const cells = selectedPlayers.map(player => {
|
|
const value = player[stat.key] || 0;
|
|
const isBest = value === bestValue && selectedPlayers.length > 1;
|
|
return `<td class="${isBest ? 'stat-best' : ''}">${stat.format(value)}</td>`;
|
|
}).join('');
|
|
|
|
return `<tr><td>${stat.label}</td>${cells}</tr>`;
|
|
}).join('');
|
|
|
|
document.getElementById('playerTableHead').innerHTML = headerRow;
|
|
document.getElementById('playerTableBody').innerHTML = bodyRows;
|
|
}
|
|
|
|
function updateVehicleComparison() {
|
|
const resultDiv = document.getElementById('vehicleComparisonResult');
|
|
const placeholderDiv = document.getElementById('vehicleComparisonPlaceholder');
|
|
|
|
if (selectedVehicles.length < 2) {
|
|
resultDiv.style.display = 'none';
|
|
placeholderDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
displayVehicleComparisonTable();
|
|
resultDiv.style.display = 'block';
|
|
placeholderDiv.style.display = 'none';
|
|
}
|
|
|
|
function updateSquadronComparison() {
|
|
const resultDiv = document.getElementById('squadronComparisonResult');
|
|
const placeholderDiv = document.getElementById('squadronComparisonPlaceholder');
|
|
|
|
if (selectedSquadrons.length < 2) {
|
|
resultDiv.style.display = 'none';
|
|
placeholderDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
displaySquadronComparisonTable();
|
|
resultDiv.style.display = 'block';
|
|
placeholderDiv.style.display = 'none';
|
|
}
|
|
|
|
function displayVehicleComparisonTable() {
|
|
const stats = [
|
|
{ label: __t('common.totalBattles'), key: 'total_battles', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('leaderboard.avgWinRateShort'), key: 'avg_win_rate', format: (val) => val.toFixed(1) + '%', higher: true },
|
|
{ label: __t('common.groundKills'), key: 'ground_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.airKills'), key: 'air_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.totalKills'), key: 'total_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.players'), key: 'players', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('leaderboard.avgKillsPlayerShort'), key: 'avg_kills', format: (val) => val.toFixed(1), higher: true }
|
|
];
|
|
|
|
const headerRow = `
|
|
<tr>
|
|
<th>${__t('leaderboard.statistic')}</th>
|
|
${selectedVehicles.map(vehicle => `<th data-vehicle-internal="${escapeHtml(vehicle.vehicle_internal || '')}" style="font-family:'skyquakesymbols','Inter',sans-serif;">${escapeHtml(vehicle.vehicle)}</th>`).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
const bodyRows = stats.map(stat => {
|
|
const values = selectedVehicles.map(vehicle => vehicle[stat.key] || 0);
|
|
const bestValue = stat.higher ? Math.max(...values) : Math.min(...values);
|
|
|
|
const cells = selectedVehicles.map(vehicle => {
|
|
const value = vehicle[stat.key] || 0;
|
|
const isBest = value === bestValue && selectedVehicles.length > 1;
|
|
return `<td class="${isBest ? 'stat-best' : ''}">${stat.format(value)}</td>`;
|
|
}).join('');
|
|
|
|
return `<tr><td>${stat.label}</td>${cells}</tr>`;
|
|
}).join('');
|
|
|
|
document.getElementById('vehicleTableHead').innerHTML = headerRow;
|
|
document.getElementById('vehicleTableBody').innerHTML = bodyRows;
|
|
}
|
|
|
|
function displaySquadronComparisonTable() {
|
|
const stats = [
|
|
{ label: __t('common.members'), key: 'player_count', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.totalBattles'), key: 'total_battles', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.wins'), key: 'wins', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.winRate'), key: 'win_rate', format: (val) => val.toFixed(1) + '%', higher: true },
|
|
{ label: __t('common.totalKills'), key: 'total_kills', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('leaderboard.totalDeaths'), key: 'deaths', format: (val) => formatNumber(val), higher: false },
|
|
{ label: __t('common.kdr'), key: 'kdr', format: (val) => val.toFixed(2), higher: true },
|
|
{ label: __t('leaderboard.totalAssists'), key: 'assists', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('leaderboard.totalCaptures'), key: 'captures', format: (val) => formatNumber(val), higher: true },
|
|
{ label: __t('common.points'), key: 'points_total', format: (val) => formatNumber(val), higher: true, calculate: (s) => s.points?.total_points || 0 },
|
|
{ label: __t('leaderboard.avgKillsMember'), key: 'avg_kills_per_member', format: (val) => val.toFixed(1), higher: true, calculate: (s) => s.player_count > 0 ? s.total_kills / s.player_count : 0 },
|
|
{ label: __t('leaderboard.avgBattlesMember'), key: 'avg_battles_per_member', format: (val) => val.toFixed(1), higher: true, calculate: (s) => s.player_count > 0 ? s.total_battles / s.player_count : 0 }
|
|
];
|
|
|
|
const headerRow = `
|
|
<tr>
|
|
<th>${__t('leaderboard.statistic')}</th>
|
|
${selectedSquadrons.map(squadron => `<th><span style="font-family:'skyquakesymbols','Inter',sans-serif;">${escapeHtml(squadron.tag_name)}</span></th>`).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
const bodyRows = stats.map(stat => {
|
|
const values = selectedSquadrons.map(squadron => {
|
|
if (stat.calculate) {
|
|
return stat.calculate(squadron);
|
|
}
|
|
return squadron[stat.key] || 0;
|
|
});
|
|
const bestValue = stat.higher ? Math.max(...values) : Math.min(...values);
|
|
|
|
const cells = selectedSquadrons.map((squadron, index) => {
|
|
const value = values[index];
|
|
const isBest = value === bestValue && selectedSquadrons.length > 1;
|
|
return `<td class="${isBest ? 'stat-best' : ''}">${stat.format(value)}</td>`;
|
|
}).join('');
|
|
|
|
return `<tr><td>${stat.label}</td>${cells}</tr>`;
|
|
}).join('');
|
|
|
|
document.getElementById('squadronTableHead').innerHTML = headerRow;
|
|
document.getElementById('squadronTableBody').innerHTML = bodyRows;
|
|
}
|
|
|
|
function handlePlayerKeydown(event) {
|
|
if (event.key === 'Escape') {
|
|
hideSearchResults();
|
|
document.getElementById('playerSearch').blur();
|
|
}
|
|
}
|
|
|
|
async function handleVehicleKeydown(event) {
|
|
if (event.key === 'Escape') {
|
|
hideSearchResults();
|
|
document.getElementById('vehicleSearch').blur();
|
|
} else if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
const input = document.getElementById('vehicleSearch');
|
|
const exact = input.value.trim().toLowerCase();
|
|
if (!exact) return;
|
|
await ensureComparisonVehicleList();
|
|
if (window.vehicleI18n && window.vehicleI18n.ensureLoaded) await window.vehicleI18n.ensureLoaded();
|
|
const match = comparisonVehicleList.find(v => vehicleDisplay(v.vehicle_internal, v.vehicle_internal).toLowerCase() === exact);
|
|
if (match) addVehicle(match.vehicle_internal);
|
|
}
|
|
}
|
|
|
|
function handleSquadronKeydown(event) {
|
|
if (event.key === 'Escape') {
|
|
document.getElementById('squadronDropdown').style.display = 'none';
|
|
document.getElementById('squadronSearch').blur();
|
|
}
|
|
}
|
|
|
|
function formatNumber(num) {
|
|
if (num >= 1000000) {
|
|
return (num / 1000000).toFixed(1) + 'M';
|
|
} else if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
}
|
|
return num.toLocaleString();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function initializeComparison() {
|
|
updateSelectedPlayers();
|
|
updateSelectedVehicles();
|
|
updateSelectedSquadrons();
|
|
}
|
|
|
|
document.addEventListener('click', function(event) {
|
|
if (!event.target.closest('.search-container')) {
|
|
hideSearchResults();
|
|
}
|
|
|
|
if (event.target.closest('.selected-item-remove')) {
|
|
const button = event.target.closest('.selected-item-remove');
|
|
const type = button.getAttribute('data-type');
|
|
|
|
if (type === 'player') {
|
|
const uid = button.getAttribute('data-player-uid');
|
|
removePlayer(uid);
|
|
} else if (type === 'vehicle') {
|
|
const vehicleInternal = button.getAttribute('data-vehicle-internal');
|
|
removeVehicle(vehicleInternal);
|
|
} else if (type === 'squadron') {
|
|
const squadronName = button.getAttribute('data-squadron-name');
|
|
removeSquadron(squadronName);
|
|
} else if (type === 'player-vehicle-combo') {
|
|
const comboKey = button.getAttribute('data-combo-key');
|
|
removePlayerVehicleCombo(comboKey);
|
|
}
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setTimeout(() => {
|
|
loadComparisonData().then(() => {
|
|
initializeComparison();
|
|
});
|
|
}, 100);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|