您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Парсинг крафтеров с сортировкой, выбором режима и изменяемой по высоте таблицей. Заголовок состоит из 6 столбцов (последний – с кнопкой закрытия). В теле таблицы весь последний столбец (ячейки (1,6), (2,6), …) является областью для перетаскивания, которая позволяет перемещать таблицу без резких скачков.
// ==UserScript== // @name HeroesWM Clan Crafters v3 + sort (Draggable, Resizable Table – Last Column Drag) // @namespace http://tampermonkey.net/ // @version 5.9 // @description Парсинг крафтеров с сортировкой, выбором режима и изменяемой по высоте таблицей. Заголовок состоит из 6 столбцов (последний – с кнопкой закрытия). В теле таблицы весь последний столбец (ячейки (1,6), (2,6), …) является областью для перетаскивания, которая позволяет перемещать таблицу без резких скачков. // @author Your Name // @include /^https{0,1}:\/\/(www|my|mirror)\.(heroeswm|178\.248\.235\.15|lordswm)\.(ru|com)\/(clan_info)\.php* // @grant GM_addStyle // @grant GM_log // @connect heroeswm.ru // @license MIT // ==/UserScript== (function() { 'use strict'; const CLAN_ID = new URL(window.location.href).searchParams.get('id'); const STORAGE_KEY = `heroeswm-crafters-${CLAN_ID}`; const isMobile = window.matchMedia('(max-width: 768px)').matches; // Переменные для сортировки let currentData = null; let currentSortKey = null; let currentSortDirection = 'asc'; GM_addStyle(` /* Контейнер таблицы */ #craftersTable { position: fixed; left: 10px; top: 50%; transform: translateY(-50%); background: white; border: 2px solid #8b4513; z-index: 9999; padding: 0; box-shadow: 3px 3px 5px rgba(0,0,0,0.3); max-width: calc(100vw - 20px); max-height: calc(100vh - 20px); overflow: auto; } /* Ресайз-хэндлы для изменения высоты */ .resize-handle { height: 5px; width: 100%; cursor: ns-resize; background: transparent; } .resize-handle:hover { background: rgba(139,69,19,0.2); } /* Таблица внутри контейнера */ #craftersTable table { border-collapse: collapse; font-size: 12px; width: 100%; } #craftersTable th, #craftersTable td { border: 1px solid #8b4513; padding: 4px 8px; text-align: center; word-break: break-word; } #craftersTable th { background-color: #deb887; cursor: pointer; } /* Панель управления */ .anvil-btn { cursor: pointer; background: #deb887; padding: 5px 10px; border: 2px solid #8b4513; border-radius: 3px; margin: 2px; display: inline-flex; align-items: center; font-size: 14px; } .buttons-container { display: flex; gap: 5px; margin: 10px 0; justify-content: center; flex-wrap: wrap; } .progress { background: #deb887; color: black; padding: 5px 10px; border: 2px solid #8b4513; border-radius: 3px; font-size: 14px; display: inline-flex; align-items: center; } /* Заголовок таблицы – одна строка с 6 столбцами. Последний столбец содержит только кнопку закрытия */ thead tr { height: 30px; line-height: 30px; user-select: none; background: #deb887; } th.close-btn { width: 30px; height: 30px; padding: 0; margin: 0; cursor: pointer; background: #deb887; border: 2px solid #8b4513; border-radius: 3px; font-size: 16px; text-align: center; vertical-align: middle; } /* Тело таблицы: каждая строка имеет 6 ячеек; последний столбец пустой */ @media (max-width: 768px) { #craftersTable { position: relative; left: auto; top: auto; transform: none; margin: 10px auto; width: 100%; max-width: 100%; } } `); function findClanBalanceContainer() { return document.querySelector('td:has(img[src*="gold.gif"])')?.closest('tr') || document.body; } // Создаем панель управления const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'buttons-container'; const viewBtn = createButton('⚒', 'Показать/скрыть таблицу', 'viewBtn'); const parseBtn = createButton('↺', 'Запустить парсинг', 'parseBtn'); // Чекбокс для режима последовательного выполнения (по умолчанию не отмечен = параллельно) const seqLabel = document.createElement('label'); seqLabel.className = 'anvil-btn'; seqLabel.style.userSelect = 'none'; const sequentialCheckbox = document.createElement('input'); sequentialCheckbox.type = 'checkbox'; sequentialCheckbox.id = 'sequentialMode'; sequentialCheckbox.style.marginRight = '5px'; seqLabel.appendChild(sequentialCheckbox); seqLabel.appendChild(document.createTextNode('Последовательно')); const progress = document.createElement('div'); progress.className = 'progress'; progress.textContent = 'Обработано: 0/0, Время: 0.000 сек'; buttonsContainer.append(parseBtn, viewBtn, seqLabel, progress); if (isMobile) { const balanceContainer = findClanBalanceContainer(); if (balanceContainer) balanceContainer.after(buttonsContainer); } else { document.body.appendChild(buttonsContainer); } function createButton(icon, title, id) { const btn = document.createElement('div'); btn.innerHTML = icon; btn.className = 'anvil-btn'; btn.id = id; btn.title = title; return btn; } // fetchDocument с декодированием из windows-1251 function fetchDocument(url) { const startTime = performance.now(); return fetch(url, { credentials: 'include' }) .then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.arrayBuffer(); }) .then(arrayBuffer => { const decoder = new TextDecoder('windows-1251'); const text = decoder.decode(arrayBuffer); const networkTime = performance.now() - startTime; const parseStart = performance.now(); const doc = new DOMParser().parseFromString(text, 'text/html'); const parseTime = performance.now() - parseStart; const totalTime = performance.now() - startTime; return { doc, text, networkTime, parseTime, totalTime }; }); } async function processPlayerLink(link) { try { const result = await fetchDocument(link); const { doc } = result; const playerName = extractPlayerName(doc); const crafts = parseCrafts(doc); const playerData = hasActiveCrafts(crafts) ? createPlayerData(playerName, crafts) : null; return { playerData, totalTime: result.totalTime }; } catch(e) { GM_log(`Ошибка парсинга ${link}: ${e}`); return { playerData: null, totalTime: 0 }; } } async function parseClanCrafters() { const startTime = performance.now(); let countRequests = 0; try { parseBtn.style.pointerEvents = 'none'; progress.style.display = 'inline-flex'; progress.textContent = 'Обработано: 0/0, Время: 0.000 сек'; const clanResult = await fetchDocument(window.location.href); const clanPage = clanResult.doc; const playerLinks = getPlayerLinks(clanPage); if (!playerLinks.length) { alert('Не найдено игроков!'); return; } progress.textContent = `Обработано: 0/${playerLinks.length}, Время: 0.000 сек`; const sequentialMode = sequentialCheckbox.checked; let responses = []; if (sequentialMode) { for (const link of playerLinks) { const res = await processPlayerLink(link); responses.push(res); countRequests++; progress.textContent = `Обработано: ${countRequests}/${playerLinks.length}, Время: ${((performance.now()-startTime)/1000).toFixed(3)} сек`; } } else { const promises = playerLinks.map(link => processPlayerLink(link).then(res => { countRequests++; progress.textContent = `Обработано: ${countRequests}/${playerLinks.length}, Время: ${((performance.now()-startTime)/1000).toFixed(3)} сек`; return res; }) ); responses = await Promise.all(promises); } const results = responses .map(r => r.playerData) .filter(item => item !== null); saveAndDisplay(results); const overallTime = ((performance.now()-startTime)/1000).toFixed(3); progress.textContent = `Обработано: ${playerLinks.length}/${playerLinks.length}, Время: ${overallTime} сек`; } catch(e) { alert('Ошибка: ' + e.message); } finally { parseBtn.style.pointerEvents = 'auto'; } } function getPlayerLinks(doc) { try { return [...doc.querySelectorAll('a[href*="pl_info.php?id="]')] .map(a => new URL(a.href, window.location.href).toString()) .filter((v, i, a) => a.indexOf(v) === i); } catch(e) { GM_log('Ошибка извлечения ссылок:', e); return []; } } function extractPlayerName(doc) { const title = doc.querySelector('title')?.textContent || ''; const match = title.match(/^([^|│]+)/); return match?.[1]?.trim() || 'Неизвестно'; } function parseCrafts(doc) { const crafts = { blacksmith: 0, craft: 0, weapons: 0, armor: 0, jeweler: 0 }; const nodes = doc.evaluate( "//td[contains(., 'Гильдия') or contains(., 'Мастер') or contains(., 'Ювелир')]", doc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); for (let i = 0; i < nodes.snapshotLength; i++) { const td = nodes.snapshotItem(i); const text = td.textContent.trim(); updateCrafts(crafts, text); } return crafts; } function updateCrafts(crafts, text) { const patterns = { blacksmith: /Гильдия Кузнецов.*?(\d+)/, craft: /Гильдия Оружейников.*?(\d+)/, weapons: /Мастер оружия.*?(\d+)/, armor: /Мастер доспехов.*?(\d+)/, jeweler: /Ювелир.*?(\d+)/ }; for (const [key, regex] of Object.entries(patterns)) { const match = text.match(regex); if (match) crafts[key] = Math.max(crafts[key], parseInt(match[1])); } } function createPlayerData(name, crafts) { const guildCraft = crafts.craft; return { name: name, crafts: crafts, totalScore: crafts.blacksmith + guildCraft + crafts.weapons + crafts.armor + crafts.jeweler, blacksmithScore: Math.min(10 + crafts.blacksmith * 10, 90), weaponScore: Math.min(1 + guildCraft, 5) * Math.min(1 + crafts.weapons, 12), armorScore: Math.min(1 + guildCraft, 5) * Math.min(1 + crafts.armor, 12), jewelerScore: Math.min(1 + guildCraft, 5) * Math.min(1 + crafts.jeweler, 12) }; } function hasActiveCrafts(crafts) { return Object.values(crafts).some(v => v > 0); } function saveAndDisplay(data) { data.sort((a, b) => b.totalScore - a.totalScore); localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); showResultsTable(data); } function showResultsTable(data) { currentData = data; // Сохраняем позицию таблицы (если уже существует) let savedLeft = null, savedTop = null; const existing = document.getElementById('craftersTable'); if (existing) { savedLeft = existing.style.left; savedTop = existing.style.top; existing.remove(); } if (!data.length) { alert('Нет данных для отображения'); return; } const headers = ['Игрок', 'Кузня', 'Оружие', 'Броня', 'Ювелирка', 'Крестик']; const sortKeys = ['name', 'blacksmithScore', 'weaponScore', 'armorScore', 'jewelerScore']; let headerHTML = `<tr>`; headers.forEach((h, i) => { if (i === headers.length - 1) { headerHTML += `<th class="close-btn" id="closeBtn">✖</th>`; } else { let arrow = ''; if (currentSortKey === sortKeys[i]) { arrow = currentSortDirection === 'asc' ? ' ▲' : ' ▼'; } headerHTML += `<th>${h}${arrow}</th>`; } }); headerHTML += `</tr>`; // Формируем тело таблицы (последний столбец остаётся пустым) let tbodyHTML = data.map((player) => { return `<tr> <td>${player.name}</td> <td>${calcForge(player.crafts.blacksmith)}</td> <td>${formatCraft(player.crafts.craft, player.crafts.weapons)}</td> <td>${formatCraft(player.crafts.craft, player.crafts.armor)}</td> <td>${formatCraft(player.crafts.craft, player.crafts.jeweler, 12)}</td> <td></td> </tr>`; }).join(''); const container = document.createElement('div'); container.id = 'craftersTable'; // Используем сохранённые координаты, если они есть container.style.left = savedLeft ? savedLeft : '10px'; container.style.top = savedTop ? savedTop : '50%'; if (!container.style.height) { container.style.height = '400px'; } container.innerHTML = ` <div class="resize-handle top"></div> <table> <thead> ${headerHTML} </thead> <tbody> ${tbodyHTML} </tbody> </table> <div class="resize-handle bottom"></div> `; document.body.appendChild(container); // Обработчик кнопки закрытия const closeBtn = container.querySelector('#closeBtn'); closeBtn.addEventListener('click', function(e) { e.stopPropagation(); container.remove(); }); // Прикрепляем обработчик перетаскивания ко всем ячейкам последнего столбца тела таблицы attachDragToLastColumn(container); // Обработчики клика по заголовкам для сортировки (исключая крестик) container.querySelectorAll('th').forEach((th, index) => { if (!th.classList.contains('close-btn')) { th.addEventListener('click', () => handleSort(index)); } }); addResizeHandlers(container); } function attachDragToLastColumn(container) { const cells = container.querySelectorAll('tbody tr td:last-child'); cells.forEach(cell => { cell.style.cursor = 'move'; cell.addEventListener('pointerdown', function(e) { e.preventDefault(); e.stopPropagation(); const startX = e.clientX; const startY = e.clientY; const initialLeft = container.offsetLeft; const initialTop = container.offsetTop; cell.setPointerCapture(e.pointerId); function onPointerMove(e) { e.preventDefault(); container.style.left = (initialLeft + (e.clientX - startX)) + 'px'; container.style.top = (initialTop + (e.clientY - startY)) + 'px'; } function onPointerUp(e) { e.preventDefault(); cell.releasePointerCapture(e.pointerId); cell.removeEventListener('pointermove', onPointerMove); cell.removeEventListener('pointerup', onPointerUp); } cell.addEventListener('pointermove', onPointerMove); cell.addEventListener('pointerup', onPointerUp); }); }); } function handleSort(columnIndex) { if (!currentData) return; const sortKeys = ['name', 'blacksmithScore', 'weaponScore', 'armorScore', 'jewelerScore']; const sortKey = sortKeys[columnIndex]; if (currentSortKey === sortKey) { currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; } else { currentSortKey = sortKey; currentSortDirection = 'asc'; } const sortedData = [...currentData].sort((a, b) => { let aValue, bValue; if (sortKey === 'name') { aValue = a.name.toLowerCase(); bValue = b.name.toLowerCase(); return currentSortDirection === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } else { aValue = a[sortKey]; bValue = b[sortKey]; return currentSortDirection === 'asc' ? aValue - bValue : bValue - aValue; } }); showResultsTable(sortedData); } function calcForge(level) { return Math.min(10 + level * 10, 90) + '%'; } function formatCraft(guildLevel, masterLevel, max = 12) { if (guildLevel === 0 || masterLevel === 0) return '-'; const g = Math.min(1 + guildLevel, 5); const m = Math.min(1 + masterLevel, max); const suffix = masterLevel >= max ? '+р' : ''; return `${g}×${m}%${suffix}`; } // Функция изменения высоты (ресайз) остаётся без изменений function addResizeHandlers(container) { const topHandle = container.querySelector('.resize-handle.top'); const bottomHandle = container.querySelector('.resize-handle.bottom'); if (!container.style.height) { container.style.height = container.offsetHeight + 'px'; } topHandle.addEventListener('mousedown', function(e) { e.preventDefault(); const startY = e.clientY; const startHeight = container.offsetHeight; const startTop = container.offsetTop; function onMouseMove(e) { const dy = e.clientY - startY; const newHeight = startHeight - dy; container.style.height = newHeight + 'px'; container.style.top = (startTop + dy) + 'px'; } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); bottomHandle.addEventListener('mousedown', function(e) { e.preventDefault(); const startY = e.clientY; const startHeight = container.offsetHeight; function onMouseMove(e) { const dy = e.clientY - startY; const newHeight = startHeight + dy; container.style.height = newHeight + 'px'; } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); } viewBtn.addEventListener('click', () => { const table = document.getElementById('craftersTable'); if (table) { table.remove(); } else { const data = JSON.parse(localStorage.getItem(STORAGE_KEY)); data ? showResultsTable(data) : alert('Данные не найдены!'); } }); parseBtn.addEventListener('click', () => { if (confirm('Обновить данные? Это может занять несколько минут.')) { parseClanCrafters(); } }); window.addEventListener('load', () => { if (localStorage.getItem(STORAGE_KEY)) viewBtn.style.display = 'block'; }); })();