您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在FFLogs带phase参数的页面添加对应阶段的真实百分位列
// ==UserScript== // @name FFLogs 添加精确百分位显示 // @namespace http://tampermonkey.net/ // @version 0.5 // @description 在FFLogs带phase参数的页面添加对应阶段的真实百分位列 // @author The.D // @match https://cn.fflogs.com/reports/* // @match https://www.fflogs.com/reports/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect fflogs.com // @connect cn.fflogs.com // @connect www.fflogs.com // @connect raw.githubusercontent.com // @license MIT // @homepage https://github.com/The-D66/fflogs-phase-color-show // @supportURL https://github.com/The-D66/fflogs-phase-color-show/issues // ==/UserScript== (function () { 'use strict'; // 添加样式 GM_addStyle(` .percentile-column { text-align: center; } .legendary { color: #ff8000 !important; } .mythic { color: #e268a8 !important; } .epic { color: #a335ee !important; } .rare { color: #0070ff !important; } .uncommon { color: #00ff96 !important; } .common { color: #9d9d9d !important; } `); // 职业类名对照表(CSS类名 -> 中英文名称) const JOB_SPECS = { // CSS类名 -> [中文名称, 英文名称] 'Warrior': ['战士', 'Warrior'], 'Paladin': ['骑士', 'Paladin'], 'DarkKnight': ['暗黑骑士', 'Dark Knight'], 'Gunbreaker': ['绝枪战士', 'Gunbreaker'], 'WhiteMage': ['白魔法师', 'White Mage'], 'Scholar': ['学者', 'Scholar'], 'Astrologian': ['占星术士', 'Astrologian'], 'Sage': ['贤者', 'Sage'], 'Monk': ['武僧', 'Monk'], 'Dragoon': ['龙骑士', 'Dragoon'], 'Ninja': ['忍者', 'Ninja'], 'Samurai': ['武士', 'Samurai'], 'Reaper': ['钐镰客', 'Reaper'], 'Bard': ['吟游诗人', 'Bard'], 'Machinist': ['机工士', 'Machinist'], 'Dancer': ['舞者', 'Dancer'], 'BlackMage': ['黑魔法师', 'Black Mage'], 'Summoner': ['召唤师', 'Summoner'], 'RedMage': ['赤魔法师', 'Red Mage'], 'Pictomancer': ['绘灵法师', 'Pictomancer'], 'Viper': ['蝰蛇剑士', 'Viper'], 'LimitBreak': ['极限技', 'Limit Break'] }; // 默认DPS值 - 用于无法获取数据时的备用值 const DEFAULT_DPS_VALUES = { }; // 反向映射:中文名称 -> CSS类名 const CN_TO_CLASS = {}; // 反向映射:英文名称 -> CSS类名 const EN_TO_CLASS = {}; // 标准化文本(转小写并移除空格) function normalizeText(text) { return text.toLowerCase().replace(/\s+/g, ''); } // 构建反向映射 Object.entries(JOB_SPECS).forEach(([cssClass, [cnName, enName]]) => { // 标准名称 CN_TO_CLASS[cnName] = cssClass; EN_TO_CLASS[enName] = cssClass; // 标准化的名称(小写且无空格) const normalizedCN = normalizeText(cnName); const normalizedEN = normalizeText(enName); // 添加标准化后的名称映射 if (normalizedCN !== cnName) { CN_TO_CLASS[normalizedCN] = cssClass; } if (normalizedEN !== enName) { EN_TO_CLASS[normalizedEN] = cssClass; } // 处理空格问题,同时支持带空格和不带空格的英文职业名 const noSpaceEnName = enName.replace(/\s+/g, ''); if (noSpaceEnName !== enName) { EN_TO_CLASS[noSpaceEnName] = cssClass; } }); // 百分位数据缓存 const percentileCache = {}; // CSV文件缓存 const csvCache = {}; // 缓存过期时间(毫秒)- 默认24小时 const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 初始化缓存 function initCache() { try { // 从localStorage加载CSV缓存 const savedCsvCache = localStorage.getItem('fflogs_csv_cache'); if (savedCsvCache) { const parsedCache = JSON.parse(savedCsvCache); // 检查缓存是否过期 if (parsedCache.timestamp && (Date.now() - parsedCache.timestamp < CACHE_EXPIRY)) { Object.assign(csvCache, parsedCache.data); console.log('已从localStorage加载CSV缓存'); } else { console.log('CSV缓存已过期,将重新获取'); } } } catch (error) { console.error('加载CSV缓存失败:', error); } } // 保存缓存到localStorage function saveCache() { try { const cacheData = { timestamp: Date.now(), data: csvCache }; localStorage.setItem('fflogs_csv_cache', JSON.stringify(cacheData)); console.log('CSV缓存已保存到localStorage'); } catch (error) { console.error('保存CSV缓存失败:', error); } } // 检查是否在带phase参数的页面上 function isPhaseReport() { return window.location.href.includes('phase=') && window.location.href.includes('type=damage-done'); } // 检查是否是伤害统计页面 function isDamageDonePage() { return window.location.href.includes('type=damage-done'); } // 解析URL获取关键信息 function parseUrl() { const url = window.location.href; const reportMatch = url.match(/reports\/([^?]+)/); const fightMatch = url.match(/fight=(\d+)/); const phaseMatch = url.match(/phase=(\d+)/); // 判断域名 const domain = url.includes('cn.fflogs.com') ? 'cn' : 'www'; // 尝试从URL获取boss信息,默认为当前版本raid // 实际应用中可能需要根据副本名称动态确定 const bossId = '1079'; // 默认为Fatebreaker/破命斗士 const zoneId = '65'; // 默认为当前版本raid return { reportId: reportMatch ? reportMatch[1] : null, fightId: fightMatch ? fightMatch[1] : null, phaseId: phaseMatch ? phaseMatch[1] : null, bossId: bossId, zoneId: zoneId, domain: domain }; } // 从页面提取bossId function extractBossId() { const bossIcon = document.getElementById('filter-fight-boss-icon'); if (bossIcon && bossIcon.src) { const match = bossIcon.src.match(/(\d+)-icon\.jpg$/); if (match && match[1]) { return match[1]; } } return null; } // 根据bossId获取CSV文件名前缀 function getCsvPrefix(bossId) { if (bossId === '1079') { return 'eden7.1'; } else if (bossId === '1077') { return 'omega7.1'; } // 默认返回eden7.1 return 'eden7.1'; } // 获取职业百分位数据 async function fetchJobPercentileStats(jobClass, phaseId) { const cacheKey = `${jobClass}_${phaseId}`; if (percentileCache[cacheKey]) { return percentileCache[cacheKey]; } const phaseNumber = phaseId || '1'; const bossId = extractBossId(); const csvPrefix = getCsvPrefix(bossId); const csvUrl = `https://raw.githubusercontent.com/ITX351/fflogs_phase_ranker/refs/heads/main/public/data/${csvPrefix}p${phaseNumber}.csv`; console.log('请求CSV数据:', csvUrl); try { // 检查CSV缓存 if (csvCache[csvUrl]) { console.log('使用缓存的CSV数据'); const dpsValues = parseCSVData(csvCache[csvUrl], jobClass); percentileCache[cacheKey] = dpsValues; return dpsValues; } // 使用GM_xmlhttpRequest替代fetch const csvText = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: csvUrl, onload: function (response) { if (response.status === 200) { resolve(response.responseText); } else { reject(new Error(`HTTP error! status: ${response.status}`)); } }, onerror: function (error) { reject(error); } }); }); // 保存到CSV缓存 csvCache[csvUrl] = csvText; // 保存缓存到localStorage saveCache(); const dpsValues = parseCSVData(csvText, jobClass); percentileCache[cacheKey] = dpsValues; return dpsValues; } catch (error) { console.error('获取CSV数据失败:', error); return null; } } // 解析CSV数据 function parseCSVData(csvText, jobClass) { try { // 将CSV文本分割成行 const lines = csvText.split('\n'); if (lines.length < 2) { console.warn('CSV数据格式不正确'); return null; } // 解析表头获取百分位点 const headerLine = lines[0]; const headers = headerLine.split(','); const percentilePoints = headers.slice(1).map(h => parseInt(h, 10)); // 查找职业行 let jobRow = null; for (let i = 1; i < lines.length; i++) { const line = lines[i]; const columns = line.split(','); if (columns.length > 0) { const rowJobClass = columns[0].trim(); // 检查是否匹配职业 if (rowJobClass === jobClass) { jobRow = columns; break; } // 检查中英文名称 const jobInfo = JOB_SPECS[jobClass]; if (jobInfo) { const [cnName, enName] = jobInfo; if (rowJobClass === cnName || rowJobClass === enName) { jobRow = columns; break; } } } } if (!jobRow) { console.warn(`在CSV中未找到职业 ${jobClass}`); return null; } // 提取各百分位的DPS值 const results = {}; for (let i = 0; i < percentilePoints.length; i++) { const percentile = percentilePoints[i]; const dpsValue = parseFloat(jobRow[i + 1]); if (!isNaN(dpsValue)) { results[percentile] = dpsValue; } } return results; } catch (error) { console.error('解析CSV数据出错:', error); return null; } } // 获取页面内容 function fetchPage(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, // 设置10秒超时 onload: function (response) { if (response.status === 200) { resolve(response.responseText); } else { reject(`请求失败: ${response.status}`); } }, onerror: function (error) { reject(error); }, ontimeout: function () { reject('请求超时'); } }); }); } // 根据DPS计算百分位 function calculatePercentile(rdps, percentileData) { if (!percentileData || Object.keys(percentileData).length === 0) { return '-'; } // 获取所有百分位点并排序 const percentiles = Object.keys(percentileData) .map(Number) .sort((a, b) => b - a); // 如果高于最高百分位 if (rdps >= percentileData[percentiles[0]]) { return 99; // 默认为最高的已知百分位 } // 如果低于最低百分位 if (rdps < percentileData[percentiles[percentiles.length - 1]]) { return Math.floor(percentiles[percentiles.length - 1] / 2); // 默认为最低已知百分位的一半 } // 查找合适的区间并插值 for (let i = 0; i < percentiles.length - 1; i++) { const higher = percentiles[i]; const lower = percentiles[i + 1]; if (rdps >= percentileData[lower] && rdps < percentileData[higher]) { // 线性插值计算更精确的百分位 const ratio = (rdps - percentileData[lower]) / (percentileData[higher] - percentileData[lower]); return Math.round(lower + ratio * (higher - lower)); } } return 50; // 默认值 } // 从表格行获取RDPS值 function getRDPS(row) { const rdpsCell = row.querySelector('.rdps'); if (!rdpsCell) return null; // 提取数字并移除非数字字符 const rdpsText = rdpsCell.textContent.trim(); const rdps = parseFloat(rdpsText.replace(/,/g, '')); return isNaN(rdps) ? null : rdps; } // 检查是否有source参数 function hasSourceParam() { return /source=\d+/.test(window.location.href); } // 处理表格更新 function handleTableUpdate() { // 查找所有行 const rows = document.querySelectorAll('tr[id^="main-table-row-"]'); // 检查是否需要添加百分位列 const needsPercentileColumn = rows.length > 0 && !document.querySelector('.percentile-column'); // 检查是否有百分位列但内容为空 const hasEmptyPercentileCells = document.querySelectorAll('.percentile-column span:empty').length > 0; // 检查是否有百分位列但显示"加载中..." const hasLoadingCells = Array.from(document.querySelectorAll('.percentile-column span')).some( span => span.textContent === '加载中...' ); // 检查是否在非phase页面但存在百分位列 const isNonPhasePage = !isPhaseReport(); const hasPercentileColumn = document.querySelector('.percentile-column') !== null; // 检查是否在非伤害统计页面 const isNonDamagePage = !isDamageDonePage(); // 检查URL是否包含source参数 const hasSource = hasSourceParam(); // 如果在非phase页面但存在百分位列,或者不在伤害统计页面,或者URL包含source参数,则移除所有百分位列 if ((isNonPhasePage && hasPercentileColumn) || isNonDamagePage || hasSource) { console.log('检测到非phase页面或非伤害统计页面或URL包含source参数,移除所有百分位列'); removePercentileColumns(); return; } // 如果有行但没有百分位列,或者有空的百分位单元格,或者有加载中的单元格 if (needsPercentileColumn || hasEmptyPercentileCells || hasLoadingCells) { console.log('检测到表格需要更新,重新添加百分位列'); addPercentileColumn(); } } // 移除所有百分位列 function removePercentileColumns() { const percentileColumns = document.querySelectorAll('.percentile-column'); percentileColumns.forEach(column => { column.remove(); }); } // 添加百分位列 async function addPercentileColumn() { // 等待表格加载完成 await waitForElement('tr[id^="main-table-row-"]'); const reportInfo = parseUrl(); // 查找所有行 const rows = document.querySelectorAll('tr[id^="main-table-row-"]'); // 添加表头 const tableElement = document.querySelector('table.summary-table'); if (tableElement) { // 查找表头行 const headerRow = tableElement.querySelector('thead tr'); if (headerRow && !headerRow.querySelector('.percentile-column')) { // 创建表头单元格 const headerCell = document.createElement('th'); headerCell.className = 'percentile-column all sorting ui-state-default'; headerCell.setAttribute('tabindex', '0'); headerCell.setAttribute('aria-label', 'Parse %: activate to sort column ascending'); // 创建内部HTML结构 const wrapper = document.createElement('div'); wrapper.className = 'DataTables_sort_wrapper'; wrapper.textContent = 'Parse %'; const sortIcon = document.createElement('span'); sortIcon.className = 'DataTables_sort_icon css_right ui-icon ui-icon-caret-2-n-s'; wrapper.appendChild(sortIcon); headerCell.appendChild(wrapper); // 插入到第一个位置 headerRow.insertBefore(headerCell, headerRow.firstChild); console.log('已添加百分位表头'); } } // 存储所有获取百分位的promise const promises = []; // 首先创建所有单元格,避免重排 for (const row of rows) { // 跳过已处理的行 if (row.querySelector('.percentile-column')) { continue; } // 创建百分位单元格(初始为空) const cell = document.createElement('td'); cell.className = 'main-table-performance rank percentile-column'; cell.innerHTML = '<span>加载中...</span>'; // 插入单元格 const firstCell = row.querySelector('td'); if (firstCell) { row.insertBefore(cell, firstCell); } // 获取职业名称 const jobClassElement = row.querySelector('.main-table-link a'); if (!jobClassElement) continue; // 提取职业名称 const jobClass = jobClassElement.className.trim(); // 获取rdps值 const rdps = getRDPS(row); if (rdps === null) { // 无RDPS数据,显示- updatePercentileCell(cell, '-'); continue; } // 极限技特殊处理 if (jobClass === 'LimitBreak') { updatePercentileCell(cell, '-'); continue; } // 创建一个Promise来处理百分位计算 const promise = (async () => { try { // 获取该职业在这个阶段的百分位统计数据 const percentileData = await fetchJobPercentileStats( jobClass, reportInfo.phaseId ); // 计算百分位 const percentile = calculatePercentile(rdps, percentileData); // 更新单元格 updatePercentileCell(cell, percentile); } catch (error) { console.error(`获取/计算百分位失败:`, error); updatePercentileCell(cell, '错误'); } })(); promises.push(promise); } // 等待所有百分位计算完成 try { await Promise.all(promises); } catch (error) { console.error('百分位计算出错:', error); } } // 更新百分位单元格 function updatePercentileCell(cell, percentile) { // 创建链接元素 const link = document.createElement('a'); if (percentile === '加载中...' || percentile === '错误') { link.textContent = percentile; } else { link.className = getColorClass(percentile); link.textContent = percentile; } // 清空单元格并添加链接 cell.innerHTML = ''; cell.appendChild(link); } // 根据百分位值获取颜色类 function getColorClass(percentile) { if (percentile === '-') return ''; const numPercentile = parseInt(percentile, 10); if (numPercentile >= 99) return 'legendary'; if (numPercentile >= 95) return 'mythic'; if (numPercentile >= 75) return 'epic'; if (numPercentile >= 50) return 'rare'; if (numPercentile >= 25) return 'uncommon'; return 'common'; } // 等待元素加载 function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); resolve(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 设置超时 setTimeout(() => { observer.disconnect(); resolve(); }, 10000); }); } // 处理phase报告页面 async function processPhaseReport() { console.log('FFLogs百分位显示脚本已加载'); // 等待页面完全加载 await new Promise(r => setTimeout(r, 2000)); // 设置定期检查 setInterval(() => { handleTableUpdate(); }, 5000); // 每5秒检查一次 // 监听DOM变化 const observer = new MutationObserver((mutations) => { // 检查是否有表格相关的变化 const tableChanged = mutations.some(mutation => { // 检查目标元素是否是表格容器或其子元素 const isTableContainer = mutation.target.id === 'main-table-container'; const isTableChild = mutation.target.closest('#main-table-container'); // 检查是否有节点添加或删除 const hasNodeChanges = mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0; return (isTableContainer || isTableChild) && hasNodeChanges; }); if (tableChanged) { console.log('检测到表格变化,准备更新'); // 使用setTimeout延迟处理,避免频繁更新 setTimeout(() => { handleTableUpdate(); }, 500); } }); // 观察整个文档 observer.observe(document.body, { childList: true, subtree: true }); // 首次尝试添加百分位列 addPercentileColumn(); } // 初始化 function init() { // 初始化缓存 initCache(); // 检查是否在带phase参数的页面上 if (isPhaseReport()) { console.log('检测到phase报告页面,开始处理...'); processPhaseReport(); } } // 启动脚本 if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', init); } else { init(); } })();