您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This script adds ability information to the statistics on stats.quake.com
// ==UserScript== // @name QCStats – Ability Kills // @namespace https://github.com/aleab/ // @version 1.0.11 // @author aleab // @description This script adds ability information to the statistics on stats.quake.com // @icon https://stats.quake.com/fav/favicon-32x32.png // @icon64 https://stats.quake.com/fav/favicon-96x96.png // @match https://stats.quake.com // @match https://stats.quake.com/* // @grant none // @require https://code.jquery.com/jquery-3.3.1.min.js // @require https://gf.qytechs.cn/scripts/371849-qcstats/code/QCStats.js?version=636315 // ==/UserScript== /* jshint esversion: 6 */ /* global $:false, MutationObserver:true, aleab:false */ // VARIABLES & CONSTANTS MutationObserver = window.MutationObserver || window.WebKitMutationObserver; const REGEX_WEAPONS_PAGE = /https:\/\/stats\.quake\.com\/profile\/.+\/weapon\/?/; const REGEX_MATCHES_PAGE = /https:\/\/stats\.quake\.com\/profile\/.+\/matches\/.+/; const GAMEMODE_ALL = "ALL"; const SCORING_EVENT_ABILITYKILL = "SCORING_EVENT_ABILITYKILL"; const SCORING_EVENT_RING_OUT = "SCORING_EVENT_RING_OUT"; const SCORING_EVENT_TELEFRAG = "SCORING_EVENT_TELEFRAG"; const prop_battleReportPersonalStatistics = "battleReportPersonalStatistics"; const prop_scoringEvents = "scoringEvents"; let selectedChampion = "ALL"; let selectingChampion = false; let noUpdObjectFoundErrorLogged = false; let config = {}; //————————————————————————————————————— $(document).ready(function() { loadConfig(); aleab.qcstats.addPageChangedListener(/.*/, () => { qcMatchScoreboardObserver.disconnect(); }); aleab.qcstats.addPageChangedListener(REGEX_MATCHES_PAGE, addAbilityStatsToMatchDetails); aleab.qcstats.addPageChangedListener(REGEX_WEAPONS_PAGE, addAbilityStatsToWeaponsPage); if (REGEX_MATCHES_PAGE.test(location.href)) { addAbilityStatsToMatchDetails(); } if (REGEX_WEAPONS_PAGE.test(location.href)) { addAbilityStatsToWeaponsPage(); } }); function loadConfig() { config = aleab.qcstats.loadConfig(); // Set defaults if (config.showTooltips === undefined) { config.showTooltips = true; } aleab.qcstats.saveConfig(config); } //————————————————————————————————————— /*=============* * FUNCTIONS * *=============*/ // UTILITY FUNCTIONS function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function formatDamageNumber(n) { if (!n) { return; } if (typeof n === typeof String()) { n = Number(n); } else if (typeof n === typeof Number()) {} else { return; } if (Number.isNaN(n)) { return "N/A"; } else if (n === Number.POSITIVE_INFINITY) { return "∞"; } else if (n === Number.NEGATIVE_INFINITY) { return "-∞"; } if (n < 1e3) { n = `${n.toFixed(0)}`; } else if (n < 1e6) { n = n / 1e3; n = `${n.toFixed(n >= 10 ? 1 : 2)}k`; } else if (n < 1e9) { n = n / 1e6; n = `${n.toFixed(n >= 10 ? 1 : 2)}M`; } else if (n < 1e12) { n = n / 1e9; n = `${n.toFixed(n >= 10 ? 1 : 2)}b`; } else { n = n.toExponential(3); } return n; } function modifySvgCircle(svg, newValue) { if (!svg || newValue === undefined || newValue === null) { return; } if (typeof newValue === typeof String()) { newValue = Number(newValue); } else if (typeof newValue === typeof Number()) {} else { return; } svg.setAttribute("value", newValue); svg.value = newValue; $(svg).find("text")[0].innerHTML = Number.isNaN(newValue) ? "N/A" : `${(newValue * 100).toFixed(0)}%`; changeSvgCirclePercentage($(svg).find("circle")[1], newValue); } function changeSvgCirclePercentage(svgCircle, newValue) { if (!svgCircle || newValue === undefined || newValue === null) { return; } if (typeof newValue === typeof String()) { newValue = Number(newValue); } else if (typeof newValue === typeof Number()) {} else { return; } let dashArray = Number(svgCircle.getAttribute("stroke-dasharray")); let dashOffset = dashArray - (Number.isNaN(newValue) || !Number.isFinite(newValue) ? 0 : dashArray * newValue); svgCircle.setAttribute("stroke-dashoffset", dashOffset); svgCircle["stroke-dashoffset"] = dashOffset; } function addAccuratePercentagesTooltips() { // svg circles // Remove the current titles, if they had already been added $("svg > title.pct-tooltip").remove(); let svgElements = $.grep($("svg"), svg => { return $(svg).find("circle").length == 2 && $(svg).find("text").length == 1; }); if (svgElements && svgElements.length > 0) { $.each(svgElements, (i, svg) => { let svgValue = Number(svg.value || svg.getAttribute("value")); var title = document.createElementNS("http://www.w3.org/2000/svg", "title") $(title).addClass("pct-tooltip"); title.innerHTML = Number.isNaN(svgValue) ? "Not available" : `${(svgValue * 100).toFixed(2)}%`; svg.prepend(title); }); } } /*———————————* | MATCHES | *———————————*/ function addAbilityStatsToMatchDetails() { setTimeout(async function() { let waitingLogged = false; while ($(".profile-page .matchdetails-page").length == 0) { if (!waitingLogged) { console.log("[QCStats – Ability kills] Waiting for the match details..."); waitingLogged = true; } await sleep(100); } console.log("[QCStats – Ability kills]"); let scoreboard = $(".profile-page .matchdetails-page > .scoreboard"); qcMatchScoreboardObserver.observe(scoreboard[0], { childList: true }); qcMatchScoreboardObserver.observe(scoreboard[1], { childList: true }); }, 200); } // This MutationObserver will observe the scoreboard element in search of changes to its children to see when one of them is expanded var qcMatchScoreboardObserver = new MutationObserver(async function(mutations, observer) { if (!mutations || !mutations[0] || !mutations[0].addedNodes || mutations[0].addedNodes.length <= 0) { return; } let extendedPlayerInfo = $.grep(mutations[0].addedNodes, node => { return $(node).hasClass("extended"); })[0]; if (!extendedPlayerInfo) { return; } let blocksJQ = $(extendedPlayerInfo).find(".item-block"); if (!blocksJQ || blocksJQ.length <= 0) { return; } let weaponsBlock = $.grep(blocksJQ, block => { let h2 = $(block).find("h2")[0]; return h2 !== undefined && h2.innerHTML == "Weapons"; })[0]; if (!weaponsBlock) { return; } let rowsJQ = $(weaponsBlock).find(".item-row"); if (!rowsJQ || rowsJQ.length <= 0) { return; } // Get rows let rows = $.grep(rowsJQ, row => { return !$(row).hasClass("thead"); }); let totalRow = $.grep(rows, row => { return $(row).find("div:first-child")[0].innerText == "Total"; })[0]; let weaponRows = $.grep(rows, row => { return $(row).find("div:first-child")[0].innerText != "Total"; }); // Get total stats let totalKills = Number($(totalRow).find("div:nth-child(2)")[0].innerText); let totalDamage = Number($(totalRow).find("div:nth-child(4)")[0].innerText); // Get weapons stats let weaponKills = 0; let weaponDamage = 0; $.each(weaponRows, (i, row) => { weaponKills += Number($(row).find("div:nth-child(2)")[0].innerText); weaponDamage += Number($(row).find("div:nth-child(4)")[0].innerText); }); // Get the number of ring outs and telefrags let ringOuts = 0; let telefrags = 0; let matchId = location.href.match(/.*\/matches\/(.*)/)[1]; let playerName = $(extendedPlayerInfo.previousSibling).find("div:first-child > a")[0].innerHTML; if (matchId && playerName) { await fetch(`https://stats.quake.com/api/v2/Player/Games?id=${matchId}&playerName=${encodeURIComponent(playerName)}`) .then(async function(response) { if (response.status === 200) { await response.json().then(function(data) { let playerMatchStats = $.grep(data[prop_battleReportPersonalStatistics], (v) => v.nickname === playerName)[0]; ringOuts = playerMatchStats[prop_scoringEvents][SCORING_EVENT_RING_OUT] || 0; telefrags = playerMatchStats[prop_scoringEvents][SCORING_EVENT_TELEFRAG] || 0; }); } }); } // Calculate ability stats let abilitiesKills = totalKills - weaponKills - ringOuts - telefrags; let abilitiesDamage = totalDamage - weaponDamage; let ringOutsRow = createNewMatchItemRow("Ring Out", "color: hsl(200, 5%, 40%)", ringOuts, undefined, undefined); let telefragsRow = createNewMatchItemRow("Telefrag", "color: hsl(295, 15%, 45%)", telefrags, undefined, undefined); let abilitiesRow = createNewMatchItemRow("Abilities", "color: hsl(165, 50%, 35%)", abilitiesKills, undefined, abilitiesDamage); totalRow.parentNode.insertBefore(ringOutsRow, totalRow.nextSibling); ringOutsRow.parentNode.insertBefore(telefragsRow, ringOutsRow.nextSibling); telefragsRow.parentNode.insertBefore(abilitiesRow, telefragsRow.nextSibling); }); function createNewMatchItemRow(label, labelStyle, kills, accuracy, damage) { let row = document.createElement("div"); row.className = "item-row"; let d = document.createElement("div"); // Label d.innerText = label; d.style = labelStyle; row.appendChild(d); d = document.createElement("div"); // Kills d.innerText = kills !== undefined ? kills.toString() : "N/A"; row.appendChild(d); d = document.createElement("div"); // Accuracy d.innerText = accuracy !== undefined ? accuracy.toString() : "N/A"; row.appendChild(d); d = document.createElement("div"); // Damage d.innerText = damage !== undefined ? damage.toString() : "N/A"; row.appendChild(d); return row; } /*———————————* | WEAPONS | *———————————*/ function addAbilityStatsToWeaponsPage() { setTimeout(async function() { let waitingLogged = false; while ($(".profile-page .champion-selector").length == 0) { if (!waitingLogged) { console.log("[QCStats – Ability kills] Waiting for the weapons stats..."); waitingLogged = true; } await sleep(100); } console.log("[QCStats – Ability kills]"); let champions = $(".profile-page .champion-selector > .champion"); $.each(champions, (i, node) => { let nodeJQ = $(node); nodeJQ.mousedown(() => weaponsPageChampion_onMouseDown(nodeJQ)); nodeJQ.mouseup(() => weaponsPageChampion_onMouseUp(nodeJQ)); }); await addAbilityStatsItemToWeaponsPage(); if (config.showTooltips) { addAccuratePercentagesTooltips(); } }, 200); } async function addAbilityStatsItemToWeaponsPage() { // Check if window.upd exists; if not, wait for it until timeout (5s) let timeWaitedForUpdObject = 0; while (!window.upd) { if (timeWaitedForUpdObject > 5000) { break; } await sleep(100); timeWaitedForUpdObject += 100; } if (!window.upd) { if (!noUpdObjectFoundErrorLogged) { console.error("[QCStats] No upd object found!"); noUpdObjectFoundErrorLogged = true; } return; } let infoBoxJQ = $(".profile-page .info-box.bare"); if (!infoBoxJQ || infoBoxJQ.length <= 0) { return; } // Remove the ability item if it's already there infoBoxJQ.find(".ability-item").remove(); let weaponItemsJQ = infoBoxJQ.find(".weapon-item"); if (!weaponItemsJQ || weaponItemsJQ.length <= 0) { return; } // Get the ability image url let imageUrl = aleab.qcstats.abilityImages[selectedChampion]; if (imageUrl) { imageUrl = `https://stats.quake.com/${imageUrl}`; } else if (imageUrl === undefined) { // Don't even add a new item to the box if the selected champion doesn't have a damage ability return; } // Get the champion's ability damage types let damageTypes = aleab.qcstats.championAbilityDamageTypes[selectedChampion]; if (damageTypes === null) { damageTypes = []; $.each(aleab.qcstats.championAbilityDamageTypes, (k, v) => { if (!v) { return true; } $.each(v, (i, s) => { if (!s) { return true; } damageTypes.push(s); }); }); } // Calculate ability stats let abilityStats = { accuracy: { acc: 0, n: 0 }, killHitPercentage: { pct: 0, n: 0 }, killPercentage: { pct: 0, n: 0 }, kills: Number(window.upd.stats[selectedChampion].gameModes[GAMEMODE_ALL][SCORING_EVENT_ABILITYKILL]), damage: 0 }; $.each(damageTypes, (i, damageType) => { let d = window.upd.stats[selectedChampion].damageStatusList[damageType]; if (d.accuracy > 0.0) { abilityStats.accuracy.n++; abilityStats.accuracy.acc += Number(d.accuracy); } if (d.killhitpct > 0.0) { abilityStats.killHitPercentage.n++; abilityStats.killHitPercentage.pct += Number(d.killhitpct); } if (d.killpct > 0.0) { abilityStats.killPercentage.n++; abilityStats.killPercentage.pct += Number(d.killpct); } abilityStats.damage += Number(d.damage); }); abilityStats.accuracy = abilityStats.accuracy.acc > 0.0 ? abilityStats.accuracy.acc / abilityStats.accuracy.n : Number.NaN; abilityStats.killHitPercentage = abilityStats.killHitPercentage.pct > 0.0 ? abilityStats.killHitPercentage.pct / abilityStats.killHitPercentage.n : Number.NaN; abilityStats.killPercentage = abilityStats.killPercentage.pct > 0.0 ? abilityStats.killPercentage.pct / abilityStats.killPercentage.n : Number.NaN; console.log(`[QCStats – Ability kills] ${selectedChampion}:`, abilityStats); // Clone the last HTML item in the box let abilityItemJQ = weaponItemsJQ.last().clone(); abilityItemJQ.addClass("ability-item"); abilityItemJQ.appendTo(infoBoxJQ); // Modify the ability item let abilityItemCells = abilityItemJQ.find("div"); // - Weapon let cellJQ = $(abilityItemCells[0]); cellJQ.find(".weapon")[0].innerText = "Ability"; if (imageUrl) { let img = cellJQ.find("img")[0]; img.src = imageUrl; img.alt = "Ability"; } else { cellJQ.css("display", "flex").css("flex-flow", "column nowrap").css("justify-content", "center"); cellJQ.find(".weapon").css("margin-bottom", "0"); cellJQ.find("img").remove(); } // - Accuracy cellJQ = $(abilityItemCells[1]); modifySvgCircle(cellJQ.find("svg")[0], abilityStats.accuracy); // - Kills / Hits cellJQ = $(abilityItemCells[2]); modifySvgCircle(cellJQ.find("svg")[0], abilityStats.killHitPrecentage); // - Kill % cellJQ = $(abilityItemCells[3]); modifySvgCircle(cellJQ.find("svg")[0], abilityStats.killPercentage); // - Kills cellJQ = $(abilityItemCells[4]); cellJQ.find(".value")[0].innerText = abilityStats.kills.toString(); // - Damage cellJQ = $(abilityItemCells[5]); cellJQ.find(".value")[0].innerText = formatDamageNumber(abilityStats.damage); } function weaponsPageChampion_onMouseDown(nodeJQ) { selectingChampion = false; if (!nodeJQ || !(nodeJQ instanceof $) || nodeJQ.length <= 0) { return; } if (!$(nodeJQ[0]).hasClass("selected")) { selectingChampion = true; } } function weaponsPageChampion_onMouseUp(nodeJQ) { if (!nodeJQ || !(nodeJQ instanceof $) || nodeJQ.length <= 0) { selectingChampion = false; return; } if (selectingChampion) { selectingChampion = false; selectedChampion = nodeJQ[0].getAttribute("data-champion"); setTimeout(async function() { await addAbilityStatsItemToWeaponsPage(); if (config.showTooltips) { addAccuratePercentagesTooltips(); } }, 100); } }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址