NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name MT_Player_Stats_Widget // @namespace http://tampermonkey.net/ // @version 2.5 // @description Скрипт добавляет виджет для отображения статистики игрока на форуме игры "Мир танков" // @author Qwen2.5-Max, Nikolay (Next) Bespalov, and many thanks to @luxero for the information provided ;) // @match http://forum.tanki.su/index.php?/topic/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @connect tanki.su // @connect lesta.ru // @license MIT // @updateURL https://gist.github.com/next-ru/f22f90a0ef505340d2374b553436045a/raw/player_widget.user.js // @downloadURL https://gist.github.com/next-ru/f22f90a0ef505340d2374b553436045a/raw/player_widget.user.js // @icon64 data:image/png;base64,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 // ==/UserScript== (function() { 'use strict'; // Предварительная загрузка шрифта const fontLink = document.createElement('link'); fontLink.rel = 'stylesheet'; fontLink.href = 'https://next-ru.github.io/player_widget/MTSans-RegularCondensed.ttf'; document.head.appendChild(fontLink); // Предварительная загрузка фонового изображения const bgImage = new Image(); bgImage.src = 'https://next-ru.github.io/player_widget/bg-content.png'; // Время жизни кэша (5 минут) const CACHE_TTL = 5 * 60 * 1000; // Функция для очистки устаревшего кэша function clearOldCache() { const now = Date.now(); const keys = GM_listValues().filter(key => key.startsWith('playerCache_')); keys.forEach(key => { const cacheData = GM_getValue(key); if(now - cacheData.timestamp > CACHE_TTL) { GM_deleteValue(key); } }); } // Вызов функции очистки кэша при запуске скрипта clearOldCache(); // Добавление пользовательского шрифта и стилей const style = document.createElement('style'); style.innerHTML = ` @font-face { font-family: 'MTSans RegularCondensed'; src: url(${fontLink.href}) format('truetype'); font-weight: 400; font-style: normal; } .regular-condensed { font-family: 'MTSans RegularCondensed', sans-serif; font-weight: 400; font-style: normal; } .widget-container { display: flex; flex-direction: column; gap: 16px; } .widget-top, .widget-bottom { display: flex; gap: 8px; align-items: flex-start; } .widget-block.player { width: 250px; } .widget-block.clan { min-width: 250px; max-width: 300px; flex-grow: 1; } .value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .clan-emblem { display: flex; justify-content: flex-end; } .clan-emblem img { width: 64px; height: 64px; } `; document.head.appendChild(style); // Создание элемента для виджета let widget = document.createElement('div'); widget.id = 'player-widget'; // Уникальный ID для виджета widget.style.position = 'absolute'; // Позиционирование будет обновляться динамически widget.style.backgroundImage = `url(${bgImage.src})`; // Устанавливаем фоновое изображение widget.style.backgroundColor = '#000'; // Задаем запасной цвет фона (черный) widget.style.color = '#fff'; // Задаем цвет текста (белый) widget.style.padding = '8px'; // Добавляем внутренние отступы для лучшей читаемости widget.style.borderRadius = '8px'; // Добавляем скругление углов widget.style.fontSize = '16px'; // Устанавливаем размер шрифта для текста widget.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)'; // Добавляем тень для объемного эффекта widget.style.whiteSpace = 'nowrap'; // Запрещаем перенос текста на новую строку widget.style.display = 'none'; // Скрываем виджет при создании widget.classList.add('regular-condensed'); // Добавляем CSS-класс для дополнительных стилей document.body.appendChild(widget); // Функция для обновления позиции виджета function updateWidgetPosition(widget, target) { const offsetX = 0; // Отступ по горизонтали (в пикселях) const offsetY = 15; // Отступ по вертикали (в пикселях) const targetRect = target.getBoundingClientRect(); // Получаем координаты целевого элемента const widgetRect = widget.getBoundingClientRect(); // Получаем размеры виджета let leftPosition = targetRect.right + window.scrollX + offsetX; // Виджет справа от элемента с отступом let topPosition; // Определяем, где находится целевой элемент: в верхней или нижней части экрана const isTargetInUpperHalf = targetRect.top < (window.innerHeight / 2); if (isTargetInUpperHalf) { // Если целевой элемент в верхней половине экрана, виджет рисуем снизу topPosition = targetRect.bottom + window.scrollY + offsetY; } else { // Если целевой элемент в нижней половине экрана, виджет рисуем сверху topPosition = targetRect.top - widgetRect.height + window.scrollY - offsetY; } // Проверяем, выходит ли виджет за пределы экрана if (topPosition < 0) { // Если виджет выходит за верхний край, перемещаем его вниз topPosition = targetRect.bottom + window.scrollY + offsetY; } else if (topPosition + widgetRect.height > window.innerHeight + window.scrollY) { // Если виджет выходит за нижний край, перемещаем его вверх topPosition = targetRect.top - widgetRect.height + window.scrollY - offsetY; } // Применяем стили Object.assign(widget.style, { top: `${topPosition}px`, left: `${leftPosition}px` }); } // Функция для форматирования времени из Unix-формата function formatUnixTime(timestamp) { const date = new Date(timestamp * 1000); return date.toLocaleString(); } // Функция для обновления содержимого виджета function updateWidgetContent(nickname, spaId, regTimestamp, lastBattleAt, summaryData, statisticsData, clanId, clanInfo, playerClanInfo) { // Верхняя секция: информация об игроке const playerTopInfo = ` <div class="value">Никнейм: ${nickname} <a href="https://tanki.su/ru/community/accounts/${spaId}-${nickname}/" target="_blank"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAG5JREFUGBmFjlEKwCAMQ2fZYfwbG+z+JxD8UPE2XStm64ay/iR9SaFumUytlW3kBEQBG6D33sGr3gdqcs67DeFRaooFIdTyadGWcPj80MmwpNk0+BxS33+lFUspx6iZUjrBVzGRiIK8APZSZg4KLkdURXcsOD5MAAAAAElFTkSuQmCC" alt="Профиль игрока" style="vertical-align: middle;"></a></div> <div class="value">Дата регистрации: ${regTimestamp ? formatUnixTime(regTimestamp) : 'Неизвестно'}</div> <div class="value">Последний бой: ${lastBattleAt ? formatUnixTime(lastBattleAt) : 'Неизвестно'}</div> `; // Верхняя секция: эмблема клана (если есть) const clanTopInfo = clanInfo ? ` <img src="https://lesta.ru${clanInfo.large_emblem_url}?nocache=${Date.now()}" alt="Эмблема клана"> ` : ''; // Нижняя секция: статистика игрока const playerBottomInfo = ` <div class="value">Личный рейтинг: ${summaryData?.global_rating ?? 'Неизвестно'}</div> <div class="value">Бои: ${statisticsData?.battles_count ?? 'Неизвестно'}</div> <div class="value">Победы: ${statisticsData?.wins_count_percent != null ? `${statisticsData.wins_count_percent}%` : 'Неизвестно'}</div> <div class="value">Средний урон: ${statisticsData?.damage_dealt_avg ?? 'Неизвестно'}</div> <div class="value">Попадания: ${summaryData?.hits_ratio != null ? `${summaryData.hits_ratio}%` : 'Неизвестно'}</div> <div class="value">Средний опыт за бой: ${statisticsData?.xp_amount_avg ?? 'Неизвестно'}</div> <div class="value">Максимум уничтожено за бой: ${statisticsData?.frags_max ?? 'Неизвестно'}</div> <div class="value">Максимальный опыт за бой: ${statisticsData?.xp_max ?? 'Неизвестно'}</div> <div class="value">Максимальный урон за бой: ${statisticsData?.damage_max ?? 'Неизвестно'}</div> <div class="value">Знаки классности «Мастер»: ${summaryData?.mastery ? `${summaryData.mastery.mastery_count}/${summaryData.mastery.vehicles_count}` : 'Неизвестно'}</div> `; // Нижняя секция: статистика клана (если есть) const clanBottomInfo = clanInfo ? ` <div class="value">Клан-тег: [${clanInfo.tag ?? 'Неизвестно'}]</div> <div class="value">Название: ${clanInfo.name ?? 'Неизвестно'}</div> <div class="value">Должность: ${playerClanInfo?.localized_name ?? 'Неизвестно'}</div> <div class="value">Дней в клане: ${playerClanInfo?.days_in_clan ?? 'Неизвестно'}</div> <div class="value">Рейтинг клана: ${clanInfo.rating ?? '-'}</div> <div class="value">Позиция клана в общем рейтинге: ${clanInfo.rating_position ?? '-'}</div> <div class="value">Активные игроки: ${clanInfo.members_count ?? 'Неизвестно'}</div> <div class="value">Среднее количество боёв: ${clanInfo.average_battles_count ?? 'Неизвестно'}</div> <div class="value">Средний опыт за бой: ${clanInfo.average_xp_per_battle ?? 'Неизвестно'}</div> <div class="value">Средний урон за бой: ${clanInfo.average_damage_per_battle ?? 'Неизвестно'}</div> <div class="value">Средний процент побед: ${clanInfo?.average_win_rate != null ? `${clanInfo.average_win_rate}%` : 'Неизвестно'}</div> ` : ''; // Вставляем всё в виджет widget.innerHTML = ` <div class="widget-container"> <!-- Верхняя часть --> <div class="widget-top"> <div class="widget-block player">${playerTopInfo}</div> ${clanTopInfo ? `<div class="widget-block clan clan-emblem">${clanTopInfo}</div>` : ''} </div> <!-- Нижняя часть --> <div class="widget-bottom"> <div class="widget-block player">${playerBottomInfo}</div> ${clanBottomInfo ? `<div class="widget-block clan">${clanBottomInfo}</div>` : ''} </div> </div> `; } // Функция для получения данных из кэша function getCache(key) { const cacheData = GM_getValue(`playerCache_${key}`); if(cacheData && Date.now() - cacheData.timestamp < CACHE_TTL) { return cacheData.data; } return null; } // Функция для сохранения данных в кэш function setCache(key, value) { GM_setValue(`playerCache_${key}`, { timestamp: Date.now(), data: value }); } // Функция для отображения небольшого текстового сообщения в виджете function showWidgetMessage(text) { widget.innerHTML = text; // Устанавливаем текст сообщения widget.style.display = 'block'; // Делаем виджет видимым widget.style.minWidth = '200px'; // Минимальная ширина widget.style.minHeight = '50px'; // Минимальная высота widget.style.display = 'flex'; // Используем flexbox для центрирования widget.style.justifyContent = 'center'; // Центрируем по горизонтали widget.style.alignItems = 'center'; // Центрируем по вертикали } // Функция для получения данных об игроке async function fetchData(spaId, nickname) { const cacheKey = `${spaId}_${nickname}`; const cachedData = getCache(cacheKey); if(cachedData) { updateWidgetContent( nickname, spaId, cachedData.regTimestamp, cachedData.lastBattleAt, cachedData.summaryData, cachedData.statisticsData, cachedData.clanId, cachedData.clanInfo, cachedData.playerClanInfo ); return; } let regTimestamp = null; let lastBattleAt = null; let clanId = null; let summaryData = null; let statisticsData = null; let clanInfo = null; let playerClanInfo = null; // Параллельные запросы: profileResponse, summaryData и statisticsData const [profileResponse, summaryPromise, statisticsPromise] = await Promise.all([ // Запрос данных профиля игрока (async () => { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://tanki.su/ru/community/accounts/${spaId}-${nickname}/`, headers: { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "accept-language": "ru-RU,ru;q=0.9", "priority": "u=0, i", "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "same-origin", "sec-fetch-user": "?1", "upgrade-insecure-requests": "1" }, referrer: "https://tanki.su/ru/community/accounts/", referrerPolicy: "unsafe-url", onload: resolve, onerror: reject }); }); const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); const scripts = doc.querySelectorAll('script'); for(const script of scripts) { const scriptContent = script.textContent; if(scriptContent.includes("USER_DATA")) { const regTimestampMatch = scriptContent.match(/"reg_timestamp":\s*(\d+)/); const lastBattleAtMatch = scriptContent.match(/"last_battle_at":\s*(\d+)/); const clanIdMatch = scriptContent.match(/"id":\s*(\d+)/); if(regTimestampMatch) regTimestamp = parseInt(regTimestampMatch[1], 10); if(lastBattleAtMatch) lastBattleAt = parseInt(lastBattleAtMatch[1], 10); if(clanIdMatch) clanId = parseInt(clanIdMatch[1], 10); break; } } } catch {} })(), // Запрос данных о суммарной статистике игрока (async () => { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://tanki.su/wotup/profile/summary/?spa_id=${spaId}&battle_type=random`, headers: { "accept": "*/*", "accept-language": "ru-RU,ru;q=0.9", "priority": "u=1, i", "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-requested-with": "XMLHttpRequest" }, referrer: `https://tanki.su/ru/community/accounts/${spaId}-${nickname}/`, referrerPolicy: "unsafe-url", onload: resolve, onerror: reject }); }); const rawSummaryData = JSON.parse(response.responseText)?.data ?? null; if(rawSummaryData) { summaryData = { global_rating: rawSummaryData.global_rating, hits_ratio: rawSummaryData.hits_ratio, mastery: rawSummaryData.mastery }; } } catch {} })(), // Запрос данных о статистике игрока (async () => { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://tanki.su/wotup/profile/statistics/?spa_id=${spaId}&battle_type=random`, headers: { "accept": "*/*", "accept-language": "ru-RU,ru;q=0.9", "priority": "u=1, i", "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-requested-with": "XMLHttpRequest" }, referrer: `https://tanki.su/ru/community/accounts/${spaId}-${nickname}/`, referrerPolicy: "unsafe-url", onload: resolve, onerror: reject }); }); const rawStatisticsData = JSON.parse(response.responseText)?.data ?? null; if(rawStatisticsData) { statisticsData = { damage_max: rawStatisticsData.damage_max, wins_count_percent: rawStatisticsData.wins_count_percent, battles_count: rawStatisticsData.battles_count, damage_dealt_avg: rawStatisticsData.damage_dealt_avg, xp_amount_avg: rawStatisticsData.xp_amount_avg, frags_max: rawStatisticsData.frags_max, xp_max: rawStatisticsData.xp_max }; } } catch {} })() ]); // Если игрок в клане, запрашиваем информацию о клане if(clanId) { const [clanPromise, playersPromise] = await Promise.all([ // Запрос информации о клане (async () => { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://lesta.ru/clans/wot/${clanId}/api/claninfo/`, headers: { "accept": "application/json, text/javascript, */*; q=0.01", "accept-language": "ru-RU,ru;q=0.9", "priority": "u=1, i", "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-requested-with": "XMLHttpRequest" }, referrer: `https://lesta.ru/clans/wot/${clanId}/`, referrerPolicy: "strict-origin-when-cross-origin", onload: resolve, onerror: reject }); }); const clanData = JSON.parse(response.responseText)?.clanview ?? null; if(clanData) { clanInfo = { tag: clanData.clan.tag, name: clanData.clan.name, rating: clanData.rating.rating, average_battles_count: clanData.rating.average_battles_count, rating_position: clanData.rating.rating_position, average_xp_per_battle: clanData.rating.average_xp_per_battle, average_damage_per_battle: clanData.rating.average_damage_per_battle, average_win_rate: clanData.rating.average_win_rate, members_count: clanData.clan.members_count, large_emblem_url: clanData.clan.large_emblem_url }; } } catch {} })(), // Запрос информации о игроке в клане (async () => { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://lesta.ru/clans/wot/${clanId}/api/players/?offset=0&limit=25&o=-role&timeframe=all&battle_type=default`, headers: { "accept": "application/json, text/javascript, */*; q=0.01", "accept-language": "ru-RU,ru;q=0.9", "priority": "u=1, i", "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "x-requested-with": "XMLHttpRequest" }, referrer: `https://lesta.ru/clans/wot/${clanId}/players/`, referrerPolicy: "strict-origin-when-cross-origin", onload: resolve, onerror: reject }); }); const playersData = JSON.parse(response.responseText)?.items ?? null; const player = playersData.find(p => p.id === parseInt(spaId, 10)); if(player) { playerClanInfo = { localized_name: player.role.localized_name, days_in_clan: player.days_in_clan }; } } catch {} })() ]); } // Сохранение данных в кэш const playerData = { regTimestamp, lastBattleAt, summaryData, statisticsData, clanId, clanInfo, playerClanInfo }; setCache(cacheKey, playerData); // Обновление содержимого виджета updateWidgetContent( nickname, spaId, playerData.regTimestamp, playerData.lastBattleAt, playerData.summaryData, playerData.statisticsData, playerData.clanId, playerData.clanInfo, playerData.playerClanInfo ); } let widgetTimeout = null; // Переменная для хранения таймера const WIDGET_TIMEOUT_DELAY = 200; // Задержка перед исчезновением виджета (в миллисекундах) // Обработчик наведения мыши на элемент или виджет function handleHover(event) { const target = event.target; // Проверяем, находится ли курсор на элементе if ( target.tagName === 'SPAN' && target.parentElement && target.parentElement.classList.contains('ccw-word') ) { const parentSpan = target.parentElement; const nickname = parentSpan.getAttribute('data-value'); const authorInfo = parentSpan.closest('.author_info'); if (authorInfo) { const parentAnchor = authorInfo.querySelector('a[hovercard-spaid]'); if (parentAnchor) { const spaId = parentAnchor.getAttribute('hovercard-spaid'); // Отменяем предыдущий таймер, если он был запущен if (widgetTimeout) { clearTimeout(widgetTimeout); widgetTimeout = null; } // Показываем индикатор загрузки showWidgetMessage('Загрузка...'); updateWidgetPosition(widget, target); // Загружаем данные и обновляем позицию виджета fetchData(spaId, nickname).then(() => { updateWidgetPosition(widget, target); }); } else { // Добавляем обработку случая, когда элемент hovercard-spaid не найден showWidgetMessage('SPA ID не найден. Аккаунт удалён.'); updateWidgetPosition(widget, target); } } } // Если курсор находится на самом виджете, отменяем таймер if (target === widget || widget.contains(target)) { if (widgetTimeout) { clearTimeout(widgetTimeout); widgetTimeout = null; } } } // Обработчик ухода курсора с элемента или виджета function handleMouseOut(event) { const target = event.target; // Если курсор ушел с элемента или с виджета if ( (target.tagName === 'SPAN' && target.parentElement.classList.contains('ccw-word')) || target === widget || widget.contains(target) ) { // Запускаем таймер для скрытия виджета widgetTimeout = setTimeout(() => { widget.innerHTML = ''; // Очищаем содержимое виджета widget.style.display = 'none'; // Скрываем виджет widgetTimeout = null; // Сбрасываем таймер }, WIDGET_TIMEOUT_DELAY); } } document.addEventListener('mouseover', handleHover); document.addEventListener('mouseout', handleMouseOut); })();