add SREBOT, SHARED, TSSBOT contents (fixup for #1223)
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>
This commit is contained in:
@@ -0,0 +1,827 @@
|
||||
// Player Details Modal - Quick-peek popup for player stats
|
||||
(function () {
|
||||
let modalInjected = false;
|
||||
let currentTab = 'overview';
|
||||
|
||||
function T(key) {
|
||||
return (window.__t && window.__t(key)) || key;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatNumber(n) {
|
||||
return (n || 0).toLocaleString();
|
||||
}
|
||||
|
||||
function getWinRateColor(wr) {
|
||||
if (wr >= 70) return '#90EE90';
|
||||
if (wr >= 60) return '#A8E6CF';
|
||||
if (wr >= 50) return '#FFD700';
|
||||
if (wr >= 40) return '#FFA500';
|
||||
return '#FF6B6B';
|
||||
}
|
||||
|
||||
function getKDRColor(kdr) {
|
||||
if (kdr >= 3) return '#90EE90';
|
||||
if (kdr >= 2) return '#A8E6CF';
|
||||
if (kdr >= 1.5) return '#FFD700';
|
||||
if (kdr >= 1) return '#FFA500';
|
||||
return '#FF6B6B';
|
||||
}
|
||||
|
||||
function injectModal() {
|
||||
if (modalInjected) return;
|
||||
modalInjected = true;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.pdm-overlay {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 9999;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.pdm-overlay.pdm-visible { opacity: 1; }
|
||||
.pdm-modal {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid rgba(144,238,144,0.15);
|
||||
border-radius: 12px;
|
||||
max-width: 700px; width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.pdm-overlay.pdm-visible .pdm-modal { transform: scale(1); }
|
||||
.pdm-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(144,238,144,0.1);
|
||||
position: sticky; top: 0;
|
||||
background: #1e1e1e;
|
||||
z-index: 1;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
.pdm-header-left {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.pdm-player-name {
|
||||
font-size: 1.1rem; font-weight: 700;
|
||||
color: #F5F5DC;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.pdm-profile-link {
|
||||
font-size: 0.75rem;
|
||||
color: #90EE90;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.pdm-profile-link:hover { opacity: 1; text-decoration: underline; }
|
||||
.pdm-close {
|
||||
background: none; border: none;
|
||||
color: rgba(255,255,255,0.5);
|
||||
font-size: 1.4rem; cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pdm-close:hover { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
.pdm-tabs {
|
||||
display: flex; justify-content: center;
|
||||
padding: 0.75rem 1.25rem 0;
|
||||
}
|
||||
.pdm-tab-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border-radius: 2rem;
|
||||
padding: 3px;
|
||||
border: 1px solid rgba(144,238,144,0.2);
|
||||
}
|
||||
.pdm-tab-slider {
|
||||
position: absolute;
|
||||
top: 3px; left: 3px;
|
||||
height: calc(100% - 6px);
|
||||
background: #90EE90;
|
||||
border-radius: calc(2rem - 2px);
|
||||
transition: left 0.25s cubic-bezier(0.4,0,0.2,1), width 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||
pointer-events: none;
|
||||
}
|
||||
.pdm-tab {
|
||||
position: relative; z-index: 1;
|
||||
background: transparent; border: none;
|
||||
color: rgba(255,255,255,0.45);
|
||||
padding: 0.4rem 1.3rem;
|
||||
border-radius: calc(2rem - 2px);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem; font-weight: 600;
|
||||
transition: color 0.25s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pdm-tab.active { color: #1b1b1b; }
|
||||
.pdm-tab:hover:not(.active) { color: rgba(255,255,255,0.8); }
|
||||
.pdm-body { padding: 1rem 1.25rem 1.25rem; }
|
||||
.pdm-loading {
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
padding: 3rem; color: rgba(255,255,255,0.5);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.pdm-spinner {
|
||||
width: 32px; height: 32px;
|
||||
border: 3px solid rgba(144,238,144,0.2);
|
||||
border-top-color: #90EE90;
|
||||
border-radius: 50%;
|
||||
animation: pdm-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes pdm-spin { to { transform: rotate(360deg); } }
|
||||
.pdm-error {
|
||||
text-align: center; padding: 2rem;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
.pdm-error i { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
|
||||
/* Overview tab */
|
||||
.pdm-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.pdm-stats-grid-last-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.pdm-stats-grid-last-row .pdm-stat-card {
|
||||
flex: 0 1 calc(33.333% - 0.4rem);
|
||||
}
|
||||
.pdm-stat-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(144,238,144,0.08);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
.pdm-stat-value {
|
||||
font-size: 1.1rem; font-weight: 700;
|
||||
color: #90EE90;
|
||||
}
|
||||
.pdm-stat-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.3rem;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.pdm-stat-toggle span {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.pdm-stat-toggle span.pdm-toggle-active {
|
||||
color: #90EE90;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pdm-stat-toggle span.pdm-toggle-inactive {
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
.pdm-stat-toggle span.pdm-toggle-inactive:hover {
|
||||
color: rgba(255,255,255,0.55);
|
||||
}
|
||||
.pdm-stat-toggle .pdm-toggle-sep {
|
||||
color: rgba(255,255,255,0.15);
|
||||
cursor: default;
|
||||
}
|
||||
.pdm-stat-label {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(255,255,255,0.5);
|
||||
margin-top: 0.15rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Mini chart card */
|
||||
.pdm-chart-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(144,238,144,0.08);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 0.6rem;
|
||||
margin-top: 0.6rem;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.pdm-chart-card:hover {
|
||||
border-color: rgba(144,238,144,0.25);
|
||||
}
|
||||
.pdm-chart-label {
|
||||
font-size: 0.65rem;
|
||||
color: rgba(255,255,255,0.5);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
text-align: center;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.pdm-chart-hint {
|
||||
font-size: 0.55rem;
|
||||
color: rgba(255,255,255,0.25);
|
||||
text-align: center;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
.pdm-mini-canvas-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
}
|
||||
.pdm-mini-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Vehicles tab */
|
||||
.pdm-vehicles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.pdm-vehicle-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(144,238,144,0.08);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.75rem;
|
||||
}
|
||||
.pdm-vehicle-card.pdm-crown-card {
|
||||
border-color: rgba(255,215,0,0.3);
|
||||
background: rgba(255,215,0,0.03);
|
||||
}
|
||||
.pdm-vehicle-name {
|
||||
font-weight: 600; font-size: 0.85rem;
|
||||
color: #F5F5DC;
|
||||
margin-bottom: 0.4rem;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.pdm-crown {
|
||||
color: #FFD700;
|
||||
margin-right: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.pdm-vehicle-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.15rem 0.5rem;
|
||||
font-size: 0.72rem;
|
||||
color: rgba(255,255,255,0.6);
|
||||
}
|
||||
.pdm-vehicle-stats span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pdm-vs-val { font-weight: 600; }
|
||||
|
||||
/* Sessions tab */
|
||||
.pdm-sessions-wrap {
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.pdm-sessions-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.pdm-sessions-table th {
|
||||
text-align: left;
|
||||
padding: 0.55rem 0.6rem;
|
||||
color: rgba(255,255,255,0.5);
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid rgba(144,238,144,0.1);
|
||||
white-space: nowrap;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.pdm-sessions-table td {
|
||||
padding: 0.55rem 0.6rem;
|
||||
color: rgba(255,255,255,0.75);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pdm-sessions-table tr:hover td {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
.pdm-result-badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.pdm-result-win { background: rgba(144,238,144,0.15); color: #90EE90; }
|
||||
.pdm-result-loss { background: rgba(255,107,107,0.15); color: #ff6b6b; }
|
||||
.pdm-no-data {
|
||||
text-align: center; padding: 2rem;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
|
||||
/* Details button */
|
||||
.pdm-details-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(144,238,144,0.15);
|
||||
color: rgba(144,238,144,0.5);
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.65rem;
|
||||
margin-left: 0.4rem;
|
||||
vertical-align: middle;
|
||||
transition: all 0.2s;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pdm-details-btn:hover {
|
||||
background: rgba(144,238,144,0.12);
|
||||
color: #90EE90;
|
||||
border-color: rgba(144,238,144,0.4);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.pdm-modal::-webkit-scrollbar,
|
||||
.pdm-vehicles-grid::-webkit-scrollbar,
|
||||
.pdm-sessions-wrap::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.pdm-modal::-webkit-scrollbar-track,
|
||||
.pdm-vehicles-grid::-webkit-scrollbar-track,
|
||||
.pdm-sessions-wrap::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.pdm-modal::-webkit-scrollbar-thumb,
|
||||
.pdm-vehicles-grid::-webkit-scrollbar-thumb,
|
||||
.pdm-sessions-wrap::-webkit-scrollbar-thumb {
|
||||
background: rgba(144,238,144,0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.pdm-stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.pdm-vehicles-grid { grid-template-columns: 1fr; }
|
||||
.pdm-sessions-table { font-size: 0.75rem; }
|
||||
.pdm-sessions-table th,
|
||||
.pdm-sessions-table td { padding: 0.45rem 0.4rem; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'pdm-overlay';
|
||||
overlay.className = 'pdm-overlay';
|
||||
overlay.style.display = 'none';
|
||||
overlay.innerHTML = `
|
||||
<div class="pdm-modal" id="pdm-modal">
|
||||
<div class="pdm-header">
|
||||
<div class="pdm-header-left">
|
||||
<span class="pdm-player-name" id="pdm-player-name"></span>
|
||||
<a class="pdm-profile-link" id="pdm-profile-link" href="#">${T('playerModal.viewFullProfile')}</a>
|
||||
</div>
|
||||
<button class="pdm-close" id="pdm-close" title="${T('playerModal.close')}">×</button>
|
||||
</div>
|
||||
<div class="pdm-tabs">
|
||||
<div class="pdm-tab-group" id="pdm-tab-group">
|
||||
<div class="pdm-tab-slider" id="pdm-tab-slider"></div>
|
||||
<button class="pdm-tab active" data-tab="overview">${T('playerModal.overview')}</button>
|
||||
<button class="pdm-tab" data-tab="vehicles">${T('playerModal.vehicles')}</button>
|
||||
<button class="pdm-tab" data-tab="sessions">${T('playerModal.sessions')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdm-body" id="pdm-body">
|
||||
<div class="pdm-loading">
|
||||
<div class="pdm-spinner"></div>
|
||||
<span>${T('playerModal.loadingPlayerData')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Events
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
document.getElementById('pdm-close').addEventListener('click', closeModal);
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && overlay.style.display !== 'none') closeModal();
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
document.getElementById('pdm-tab-group').addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.pdm-tab');
|
||||
if (!btn || btn.classList.contains('active')) return;
|
||||
document.querySelectorAll('.pdm-tab').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentTab = btn.dataset.tab;
|
||||
updateSlider();
|
||||
renderTab();
|
||||
});
|
||||
|
||||
// Prevent modal scroll from propagating
|
||||
document.getElementById('pdm-modal').addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
function updateSlider() {
|
||||
const group = document.getElementById('pdm-tab-group');
|
||||
const slider = document.getElementById('pdm-tab-slider');
|
||||
const active = group.querySelector('.pdm-tab.active');
|
||||
if (!active || !slider) return;
|
||||
slider.style.left = active.offsetLeft + 'px';
|
||||
slider.style.width = active.offsetWidth + 'px';
|
||||
}
|
||||
|
||||
let playerDataCache = null;
|
||||
let gamesDataCache = null;
|
||||
let historyDataCache = null;
|
||||
let miniChart = null;
|
||||
let miniChartMetric = 'kdr';
|
||||
let currentKdrKps = 'kdr';
|
||||
const miniChartMetrics = ['kdr', 'win_rate', 'battles'];
|
||||
const miniChartConfig = {
|
||||
kdr: { label: T('playerModal.kdr'), color: '#64b5f6', suffix: '' },
|
||||
win_rate: { label: T('playerModal.winRate'), color: '#90EE90', suffix: '%' },
|
||||
battles: { label: T('playerModal.battles'), color: '#ffb74d', suffix: '' }
|
||||
};
|
||||
|
||||
function closeModal() {
|
||||
const overlay = document.getElementById('pdm-overlay');
|
||||
overlay.classList.remove('pdm-visible');
|
||||
setTimeout(() => {
|
||||
overlay.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}, 200);
|
||||
// Destroy mini chart to prevent canvas reuse issues
|
||||
if (miniChart) {
|
||||
miniChart.destroy();
|
||||
miniChart = null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTab() {
|
||||
const body = document.getElementById('pdm-body');
|
||||
if (!playerDataCache) return;
|
||||
|
||||
// Destroy old mini chart before re-rendering
|
||||
if (miniChart) {
|
||||
miniChart.destroy();
|
||||
miniChart = null;
|
||||
}
|
||||
|
||||
if (currentTab === 'overview') {
|
||||
body.innerHTML = renderOverview(playerDataCache);
|
||||
initMiniChart();
|
||||
} else if (currentTab === 'vehicles') {
|
||||
body.innerHTML = renderVehicles(playerDataCache);
|
||||
initVehicleKdrToggle();
|
||||
} else if (currentTab === 'sessions') {
|
||||
body.innerHTML = renderSessions(gamesDataCache);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOverview(data) {
|
||||
const vehicles = (data.vehicles || []).filter(v => v.vehicle !== 'DISCONNECTED');
|
||||
let totalBattles = 0, wins = 0, groundKills = 0, airKills = 0, assists = 0, deaths = 0, captures = 0;
|
||||
|
||||
vehicles.forEach(v => {
|
||||
const s = v.stats;
|
||||
totalBattles += s.total_battles || 0;
|
||||
wins += s.wins || 0;
|
||||
groundKills += s.ground_kills || 0;
|
||||
airKills += s.air_kills || 0;
|
||||
assists += s.assists || 0;
|
||||
deaths += s.deaths || 0;
|
||||
captures += s.captures || 0;
|
||||
});
|
||||
|
||||
const totalKills = groundKills + airKills;
|
||||
const winRate = totalBattles > 0 ? ((wins / totalBattles) * 100).toFixed(1) + '%' : '0.0%';
|
||||
const kdr = deaths > 0 ? (totalKills / deaths).toFixed(2) : totalKills.toFixed(2);
|
||||
const kps = totalBattles > 0 ? (totalKills / totalBattles).toFixed(2) : '0.00';
|
||||
|
||||
const stats = [
|
||||
[T('playerModal.totalBattles'), formatNumber(totalBattles)],
|
||||
[T('playerModal.wins'), formatNumber(wins)],
|
||||
[T('playerModal.winRate'), winRate],
|
||||
[T('playerModal.totalKills'), formatNumber(totalKills)],
|
||||
[T('playerModal.kdr'), kdr],
|
||||
[T('playerModal.kps'), kps],
|
||||
[T('playerModal.airKills'), formatNumber(airKills)],
|
||||
[T('playerModal.groundKills'), formatNumber(groundKills)],
|
||||
[T('playerModal.assists'), formatNumber(assists)],
|
||||
[T('playerModal.deaths'), formatNumber(deaths)],
|
||||
[T('playerModal.captures'), formatNumber(captures)],
|
||||
];
|
||||
|
||||
const cfg = miniChartConfig[miniChartMetric];
|
||||
const chartCard = `<div class="pdm-chart-card" id="pdm-chart-card" title="${T('playerModal.clickToSwitchMetric')}">
|
||||
<div class="pdm-chart-label" id="pdm-chart-metric-label">${cfg.label}</div>
|
||||
<div class="pdm-mini-canvas-wrap"><canvas class="pdm-mini-canvas" id="pdm-mini-canvas"></canvas></div>
|
||||
<div class="pdm-chart-hint">${T('playerModal.clickToCycle')}</div>
|
||||
</div>`;
|
||||
|
||||
const fullRows = stats.slice(0, Math.floor(stats.length / 3) * 3);
|
||||
const remainder = stats.slice(fullRows.length);
|
||||
const cardHtml = s => `<div class="pdm-stat-card"><div class="pdm-stat-value">${s[1]}</div><div class="pdm-stat-label">${s[0]}</div></div>`;
|
||||
const lastRow = remainder.length ? `<div class="pdm-stats-grid-last-row">${remainder.map(cardHtml).join('')}</div>` : '';
|
||||
|
||||
return `<div class="pdm-stats-grid">${fullRows.map(cardHtml).join('')}</div>${lastRow}${chartCard}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function initVehicleKdrToggle() {
|
||||
const toggle = document.getElementById('pdm-veh-kdr-toggle');
|
||||
if (!toggle) return;
|
||||
toggle.addEventListener('click', function (e) {
|
||||
const span = e.target.closest('[data-mode]');
|
||||
if (!span || span.classList.contains('pdm-toggle-active')) return;
|
||||
currentKdrKps = span.dataset.mode;
|
||||
renderTab();
|
||||
});
|
||||
}
|
||||
|
||||
function initMiniChart() {
|
||||
const card = document.getElementById('pdm-chart-card');
|
||||
if (!card) return;
|
||||
|
||||
card.addEventListener('click', function () {
|
||||
const idx = miniChartMetrics.indexOf(miniChartMetric);
|
||||
miniChartMetric = miniChartMetrics[(idx + 1) % miniChartMetrics.length];
|
||||
renderMiniChart();
|
||||
});
|
||||
|
||||
// Load history data if not cached, then render
|
||||
if (historyDataCache) {
|
||||
renderMiniChart();
|
||||
} else if (currentUid) {
|
||||
window.apiClient.request('/api/player/' + currentUid + '/history').then(data => {
|
||||
historyDataCache = data;
|
||||
renderMiniChart();
|
||||
}).catch(() => {
|
||||
const canvas = document.getElementById('pdm-mini-canvas');
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
ctx.font = '11px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(T('playerModal.noChartData'), canvas.width / 2, canvas.height / 2);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function renderMiniChart() {
|
||||
if (!historyDataCache || !historyDataCache.history || !historyDataCache.history.length) return;
|
||||
if (typeof Chart === 'undefined') return;
|
||||
|
||||
const canvas = document.getElementById('pdm-mini-canvas');
|
||||
const label = document.getElementById('pdm-chart-metric-label');
|
||||
if (!canvas) return;
|
||||
|
||||
const cfg = miniChartConfig[miniChartMetric];
|
||||
if (label) label.textContent = cfg.label;
|
||||
|
||||
const history = historyDataCache.history;
|
||||
const dataPoints = history
|
||||
.filter(d => d[miniChartMetric] != null)
|
||||
.map(d => ({ x: new Date(d.period + 'T00:00:00Z').getTime(), y: d[miniChartMetric] }));
|
||||
if (!dataPoints.length) return;
|
||||
|
||||
if (miniChart) {
|
||||
miniChart.data.datasets[0].data = dataPoints;
|
||||
miniChart.data.datasets[0].borderColor = cfg.color;
|
||||
miniChart.data.datasets[0].backgroundColor = cfg.color + '18';
|
||||
miniChart.data.datasets[0].pointBackgroundColor = cfg.color;
|
||||
miniChart.options.plugins.tooltip.borderColor = cfg.color;
|
||||
miniChart.options.plugins.tooltip.bodyColor = cfg.color;
|
||||
miniChart.options.plugins.tooltip.callbacks.label = ctx => `${cfg.label}: ${ctx.parsed.y}${cfg.suffix}`;
|
||||
miniChart.update();
|
||||
return;
|
||||
}
|
||||
|
||||
miniChart = new Chart(canvas.getContext('2d'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: dataPoints,
|
||||
borderColor: cfg.color,
|
||||
backgroundColor: cfg.color + '18',
|
||||
borderWidth: 1.5,
|
||||
pointBackgroundColor: cfg.color,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 3,
|
||||
tension: 0.15,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: { duration: 300 },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: 'rgba(15,15,15,0.92)',
|
||||
borderColor: cfg.color,
|
||||
borderWidth: 1,
|
||||
titleColor: 'rgba(255,255,255,0.75)',
|
||||
bodyColor: cfg.color,
|
||||
padding: 6,
|
||||
displayColors: false,
|
||||
callbacks: {
|
||||
title: () => '',
|
||||
label: ctx => `${cfg.label}: ${ctx.parsed.y}${cfg.suffix}`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { type: 'linear', display: false, min: (function() { var y = new Date(history[0].period + 'T00:00:00Z').getUTCFullYear(); return Date.UTC(y, 0, 1); })() },
|
||||
y: { display: false, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let currentUid = null;
|
||||
|
||||
function renderVehicles(data) {
|
||||
const vehicles = (data.vehicles || []).filter(v => v.vehicle !== 'DISCONNECTED');
|
||||
if (!vehicles.length) return `<div class="pdm-no-data"><i class="fas fa-box-open" style="font-size:2rem;margin-bottom:0.5rem;display:block;"></i>${T('playerModal.noVehicleData')}</div>`;
|
||||
|
||||
// Sort by total battles desc
|
||||
vehicles.sort((a, b) => (b.stats.total_battles || 0) - (a.stats.total_battles || 0));
|
||||
|
||||
// Find best WR vehicle with >15 games
|
||||
let bestWrVehicle = null;
|
||||
let bestWr = -1;
|
||||
vehicles.forEach(v => {
|
||||
const battles = v.stats.total_battles || 0;
|
||||
if (battles > 15) {
|
||||
const wr = (v.stats.wins || 0) / battles;
|
||||
if (wr > bestWr) {
|
||||
bestWr = wr;
|
||||
bestWrVehicle = v.vehicle;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mode = currentKdrKps || 'kdr';
|
||||
const kdrCls = mode === 'kdr' ? 'pdm-toggle-active' : 'pdm-toggle-inactive';
|
||||
const kpsCls = mode === 'kps' ? 'pdm-toggle-active' : 'pdm-toggle-inactive';
|
||||
const toggleHeader = `<div class="pdm-stat-toggle" id="pdm-veh-kdr-toggle" style="margin-bottom:0.5rem;">
|
||||
<span class="${kdrCls}" data-mode="kdr">KDR</span>
|
||||
<span class="pdm-toggle-sep">/</span>
|
||||
<span class="${kpsCls}" data-mode="kps">KPS</span>
|
||||
</div>`;
|
||||
|
||||
return `${toggleHeader}<div class="pdm-vehicles-grid">${vehicles.map(v => {
|
||||
const s = v.stats;
|
||||
const totalKills = (s.ground_kills || 0) + (s.air_kills || 0);
|
||||
const battles = s.total_battles || 0;
|
||||
const wins = s.wins || 0;
|
||||
const wrNum = battles > 0 ? (wins / battles) * 100 : 0;
|
||||
const wr = wrNum.toFixed(1) + '%';
|
||||
const kdrNum = s.deaths > 0 ? totalKills / s.deaths : totalKills;
|
||||
const kdr = kdrNum.toFixed(2);
|
||||
const kpsNum = battles > 0 ? totalKills / battles : 0;
|
||||
const kps = kpsNum.toFixed(2);
|
||||
const isCrown = v.vehicle === bestWrVehicle;
|
||||
const crownClass = isCrown ? ' pdm-crown-card' : '';
|
||||
const crownIcon = isCrown ? '<i class="fas fa-crown pdm-crown"></i>' : '';
|
||||
const wrColor = getWinRateColor(wrNum);
|
||||
const activeVal = mode === 'kdr' ? kdr : kps;
|
||||
const activeNum = mode === 'kdr' ? kdrNum : kpsNum;
|
||||
const activeColor = getKDRColor(activeNum);
|
||||
const activeLabel = mode === 'kdr' ? T('playerModal.kdr') : T('playerModal.kps');
|
||||
return `<div class="pdm-vehicle-card${crownClass}">
|
||||
<div class="pdm-vehicle-name" title="${escapeHtml(v.vehicle)}">${crownIcon}${escapeHtml(v.vehicle)}</div>
|
||||
<div class="pdm-vehicle-stats">
|
||||
<span>${T('playerModal.battles')}: <span class="pdm-vs-val" style="color:#90EE90">${battles}</span></span>
|
||||
<span>${T('playerModal.wins')}: <span class="pdm-vs-val" style="color:#90EE90">${wins}</span></span>
|
||||
<span>${T('playerModal.winRate')}: <span class="pdm-vs-val" style="color:${wrColor}">${wr}</span></span>
|
||||
<span>${activeLabel}: <span class="pdm-vs-val" style="color:${activeColor}">${activeVal}</span></span>
|
||||
<span>${T('playerModal.ground')}: <span class="pdm-vs-val" style="color:#90EE90">${s.ground_kills || 0}</span></span>
|
||||
<span>${T('playerModal.air')}: <span class="pdm-vs-val" style="color:#90EE90">${s.air_kills || 0}</span></span>
|
||||
<span>${T('playerModal.assists')}: <span class="pdm-vs-val" style="color:#90EE90">${s.assists || 0}</span></span>
|
||||
<span>${T('playerModal.deaths')}: <span class="pdm-vs-val" style="color:#90EE90">${s.deaths || 0}</span></span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderSessions(gamesData) {
|
||||
if (!gamesData || !gamesData.games || !gamesData.games.length) {
|
||||
return `<div class="pdm-no-data"><i class="fas fa-history" style="font-size:2rem;margin-bottom:0.5rem;display:block;"></i>${T('playerModal.noSessionData')}</div>`;
|
||||
}
|
||||
|
||||
const games = gamesData.games
|
||||
.slice()
|
||||
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
||||
.slice(0, 50);
|
||||
|
||||
const rows = games.map(g => {
|
||||
const date = new Date(g.timestamp * 1000);
|
||||
const fmt = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const result = g.result || T('playerModal.unknown');
|
||||
const isWin = result.toLowerCase() === 'win';
|
||||
const badgeClass = isWin ? 'pdm-result-win' : 'pdm-result-loss';
|
||||
return `<tr>
|
||||
<td>${fmt}</td>
|
||||
<td>${escapeHtml(g.vehicle || g.vehicle_internal || T('playerModal.unknown'))}</td>
|
||||
<td>${g.stats.ground_kills || 0}</td>
|
||||
<td>${g.stats.air_kills || 0}</td>
|
||||
<td>${g.stats.assists || 0}</td>
|
||||
<td>${g.stats.deaths || 0}</td>
|
||||
<td><span class="pdm-result-badge ${badgeClass}">${result}</span></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="pdm-sessions-wrap"><table class="pdm-sessions-table">
|
||||
<thead><tr>
|
||||
<th>${T('playerModal.date')}</th><th>${T('playerModal.vehicle')}</th><th>${T('playerModal.ground')}</th><th>${T('playerModal.air')}</th><th>${T('playerModal.assists')}</th><th>${T('playerModal.deaths')}</th><th>${T('playerModal.result')}</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table></div>`;
|
||||
}
|
||||
|
||||
window.openPlayerDetailsModal = async function (uid, nick) {
|
||||
injectModal();
|
||||
|
||||
const overlay = document.getElementById('pdm-overlay');
|
||||
const body = document.getElementById('pdm-body');
|
||||
const nameEl = document.getElementById('pdm-player-name');
|
||||
const linkEl = document.getElementById('pdm-profile-link');
|
||||
|
||||
// Reset state
|
||||
currentTab = 'overview';
|
||||
miniChartMetric = 'kdr';
|
||||
currentKdrKps = 'kdr';
|
||||
playerDataCache = null;
|
||||
gamesDataCache = null;
|
||||
historyDataCache = null;
|
||||
currentUid = uid;
|
||||
if (miniChart) {
|
||||
miniChart.destroy();
|
||||
miniChart = null;
|
||||
}
|
||||
document.querySelectorAll('.pdm-tab').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector('.pdm-tab[data-tab="overview"]').classList.add('active');
|
||||
|
||||
nameEl.textContent = nick || uid;
|
||||
linkEl.href = '/players/' + encodeURIComponent(uid);
|
||||
|
||||
body.innerHTML = `<div class="pdm-loading"><div class="pdm-spinner"></div><span>${T('playerModal.loadingPlayerData')}</span></div>`;
|
||||
|
||||
overlay.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
overlay.classList.add('pdm-visible');
|
||||
updateSlider();
|
||||
});
|
||||
|
||||
try {
|
||||
const [playerData, gamesData] = await Promise.all([
|
||||
window.apiClient.getPlayer(uid),
|
||||
window.apiClient.getPlayerGames(uid)
|
||||
]);
|
||||
playerDataCache = playerData;
|
||||
gamesDataCache = gamesData;
|
||||
renderTab();
|
||||
} catch (err) {
|
||||
console.error('[Player Details Modal] Error:', err);
|
||||
body.innerHTML = `<div class="pdm-error"><i class="fas fa-exclamation-triangle" style="display:block;margin-bottom:0.5rem;"></i>${T('playerModal.failedToLoadPlayerData')}</div>`;
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user