GustavBylund
+46 (0)73 026 26 86hello@gustavbylund.semaisthoCalculate your CSGO winrate
This is a bookmarklet that you can use to calculate some interesting statistics using Steams GDPR-available data.
You need to run it at steamcommunity.com
Bookmarklet
Drag the following link to your bookmark bar!
CSGO StatsOr add this code to a bookmark:
javascript:var CsgoMap;!function(e){e.de_mirage="Mirage",e.de_dust2="Dust II",e.de_subzero="Subzero",e.de_train="Train",e.de_nuke="Nuke",e.de_inferno="Inferno",e.de_cache="Cache",e.de_overpass="Overpass"}(CsgoMap=CsgoMap||{});class MinMaxAvg{min=1/0;max=-1/0;avg=0;total=0;append(e,t){this.min=Math.min(e,this.min),this.max=Math.max(e,this.max),this.total+=e,this.avg=this.total/t}}class PlayerStats{link;image;name;constructor(e){this.link=e.link,this.image=e.image,this.name=e.name}timesPlayed=0;roundsPlayed=0;ping=new MinMaxAvg;kills=new MinMaxAvg;assists=new MinMaxAvg;deaths=new MinMaxAvg;mvps=new MinMaxAvg;headshotRate=new MinMaxAvg;score=new MinMaxAvg;add(e,t){this.roundsPlayed+=t,this.timesPlayed+=1,this.ping.append(e.ping,this.timesPlayed),this.kills.append(e.kills,this.timesPlayed),this.assists.append(e.assists,this.timesPlayed),this.deaths.append(e.deaths,this.timesPlayed),this.mvps.append(e.mvps,this.timesPlayed),this.headshotRate.append(Number(e.headshotRate),this.timesPlayed),this.score.append(e.score,this.timesPlayed)}}const CACHE_KEY="__maistho_csgo_stats_cached_matches";function getCachedMatches(){try{const e=JSON.parse(localStorage.getItem(CACHE_KEY)||"");return e.forEach(e=>e.time=new Date(e.time)),console.log(e),e}catch(e){return localStorage.removeItem(CACHE_KEY),[]}}async function fetchWithRetry(e,a){for(let t=0;t<5;++t)try{return fetch(e,a)}catch(e){console.warn(e),await new Promise(e=>setTimeout(e,500*t))}}let profileLink;async function getAllMatches(){if("steamcommunity.com"!=window.location.hostname)throw window.location.href="https://steamcommunity.com/my/gcpd/730?tab=matchhistorycompetitive",new Error("Redirecting to steamcommunity.com, please run the bookmarklet again after the page has reloaded");const e=document.querySelector("#global_actions>a.user_avatar");if(!e)throw new Error("You need to be logged in");profileLink=e.href.replace(/\/$/,"");const t=getCachedMatches();var a=t.reduce((e,t)=>(e[t.time.toISOString()]=!0,e),{});const n=[];let s,o=0,i=!1;do{const c=new URLSearchParams({ajax:"1",tab:"matchhistorycompetitive"});s&&c.append("continue_token",s);var r=await fetchWithRetry(profileLink+"/gcpd/730?"+c.toString(),{credentials:"include"}).then(e=>e.json());if(!r.success)throw new Error(JSON.stringify(r));const d=new DOMParser,m=d.parseFromString(r.html,"text/html");for(const h of Array.from(m.querySelectorAll(".generic_kv_table.csgo_scoreboard_root>tbody>tr"))){const g={map:getMap(h),time:getTime(h),waitTime:getWaitTime(h),matchDuration:getMatchDuration(h),score:getScore(h),demo:getDemo(h),...getTeams(h,profileLink)};if(g.map){if(a[g.time.toISOString()]){i=!0;break}n.push(g)}}s=r.continue_token,loadingStatus.innerText=++o+" pages parsed..."}while(!i&&s);var l=[...n,...t];return localStorage.setItem(CACHE_KEY,JSON.stringify(l)),l}const getTeams=(e,t)=>{const a=[[],[]];let n=-1;for(const s of Array.from(e.querySelectorAll(".csgo_scoreboard_inner_right>tbody>tr"))){const o=q(s,"a.linkTitle");null==o?n+=1:a[n].push({link:o.attributes.getNamedItem("href").textContent||"",name:getInfo(s,"a.linkTitle"),image:getAttribute(q(s,".playerAvatar img"),"src"),ping:parseInt(getInfo(s,"td:nth-child(2)")),kills:parseInt(getInfo(s,"td:nth-child(3)")),assists:parseInt(getInfo(s,"td:nth-child(4)")),deaths:parseInt(getInfo(s,"td:nth-child(5)")),mvps:parseInt(getInfo(s,"td:nth-child(6)").substr(1))||0,headshotRate:Number(getInfo(s,"td:nth-child(7)").replace(/%$/,""))/100,score:parseInt(getInfo(s,"td:nth-child(8)"))})}e=a.findIndex(e=>e.some(e=>e.link===t));return{teams:a,userTeam:e}},getDemo=e=>{e=q(e,".csgo_scoreboard_inner_left .csgo_scoreboard_btn_gotv");return getAttribute(e&&e.parentElement,"href")},getScore=e=>getInfo(e,".csgo_scoreboard_score"),getMatchDuration=e=>getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(4)").replace(/^Match Duration: /,""),getWaitTime=e=>getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(3)").replace(/^Wait Time: /,""),getTime=e=>new Date(getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(2)").replace(/\s*GMT$/i,"")),getMap=e=>getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(1)").replace(/^Competitive /,"");function getInfo(e,t){return getText(q(e,t))}function getText(e){return(e&&e.textContent||"").trim()}function q(e,t){return e&&e.querySelector(t)}function getAttribute(e,t){return(e&&e.getAttribute(t)||"").trim()}function parseMatches(e){const c={},d={};return e.forEach(e=>{var t=e.score.split(":").map(e=>e.trim()).map(e=>Number(e)),a=t[0]+t[1],n=e.userTeam,s=0===n?1:0;c[e.map]||(c[e.map]={name:e.map,timesPlayed:0,wins:0,losses:0,draws:0,winrate:0,roundsWon:0,roundsLost:0,roundWinrate:0});const o=c[e.map];t[n]>t[s]?o.wins+=1:t[n]<t[s]?o.losses+=1:o.draws+=1,o.timesPlayed+=1,o.roundsWon+=t[n],o.roundsLost+=t[s];for(const i of e.teams)for(const r of i){d[r.link]||(d[r.link]=new PlayerStats(r));const l=d[r.link];l.add(r,a)}}),Object.keys(c).forEach(e=>{const t=c[e];t.winrate=t.wins/(t.timesPlayed-t.draws),t.roundWinrate=t.roundsWon/(t.roundsWon+t.roundsLost)}),{maps:c,players:d}}function fmt(e,t=2){return Number(e.toFixed(t))}function displayPlayer(e){return{image:`<a href="${e.link}"><img src="${e.image}"></a>`,name:`<a href="${e.link}">${e.name}</a>`,gamesPlayed:e.timesPlayed,kdr:fmt(e.kills.total/e.deaths.total),totalKills:e.kills.total,killsPerGame:fmt(e.kills.avg),killsPerRound:fmt(e.kills.total/e.roundsPlayed),totalAssists:e.assists.total,assistsPerGame:fmt(e.assists.avg),assistsPerRound:fmt(e.assists.total/e.roundsPlayed),totalDeaths:e.deaths.total,deathsPerGame:fmt(e.deaths.avg),deathsPerRound:fmt(e.deaths.total/e.roundsPlayed),headshots:fmt(e.headshotRate.avg),compareScore:`<button onclick="window.__maistho_csgo_stats_compare('${e.link}');">Compare</button>`}}function printStatistics(e,t,a){const n=t.sort((e,t)=>t.timesPlayed-e.timesPlayed).slice(0,20);var t=Object.values(e).sort((e,t)=>t.winrate-e.winrate).map(e=>({...e,winrate:formatWinrate(e.winrate),roundWinrate:formatWinrate(e.roundWinrate)})),e=(console.table(n),console.table(t),"text-align: center; margin: 2em 0 0.5em 0;"),s="overflow: auto;",t=`
<section style="${s}">
<h2 style="${e}">Map statistics</h2>
${makeTable(t)}
</section>
<section style="${s}">
<h2 style="${e}">Player statistics</h2>
${makeTable(n.map(displayPlayer))}
</section>
<section>
<h2>Most recent game</h2>
<p>${a[0].time}</p>
<h2>Earliest game</h2>
<p>${a[a.length-1].time}</p>
</section>
`,s="__maistho_csgo_stats_table_wrapper";let o=document.getElementById(s);o||((o=document.createElement("div")).id=s,o.style.background="#171a21",o.style.color="white",document.body.prepend(o)),o.innerHTML=t;const i=document.querySelector(".responsive_header");i&&(e=o.clientHeight+"px",i.style.marginTop=e)}function makeTable(e){const n=Object.keys(e[0]),s="padding: 0.125rem 0.5rem;";return`<table style="margin: 0 auto; border-collapse: collapse;">
<thead>
<tr>
${n.map(e=>{e=(""+e[0].toLocaleUpperCase()+e.substr(1)).replace(/([A-Z])/g," $1");return`<th style="${s}">${e}</th>`}).join("\n")}
</tr>
</thead>
<tbody>
${e.map((t,a)=>{return`<tr>${n.map(e=>`<td style="${s} ${a%2==0?"background-color: rgba(255,255,255,0.2);":""}">${t[e]}</td>`).join("\n")}</tr>`}).join("\n")}
</tbody>
</table>`}function formatWinrate(e){return(100*e).toLocaleString(void 0,{maximumFractionDigits:2,minimumFractionDigits:0})+"%"}const loadingSpinner=document.createElement("div"),loadingStatus=(loadingSpinner.innerHTML='<img src="https://steamcommunity-a.akamaihd.net/public/images/login/throbber.gif">',loadingSpinner.style.display="flex",loadingSpinner.style.alignItems="center",loadingSpinner.style.justifyContent="center",loadingSpinner.style.position="fixed",loadingSpinner.style.top="0",loadingSpinner.style.left="0",loadingSpinner.style.bottom="0",loadingSpinner.style.right="0",loadingSpinner.style.background="rgba(0,0,0,0.5)",loadingSpinner.style.flexDirection="column",loadingSpinner.style.zIndex="999999",document.createElement("div"));loadingStatus.innerText="0 pages parsed...",loadingStatus.style.color="white",loadingStatus.style.margin="1em",loadingSpinner.appendChild(loadingStatus),document.body.appendChild(loadingSpinner);let rawMatches,parsedData;function showModal(e){var t="__maistho_csgo_stats_modal_wrapper";let a=document.getElementById(t);a||((a=document.createElement("div")).id=t,a.style.background="rgba(0,0,0,0.5)",a.style.position="fixed",a.style.top="0",a.style.bottom="0",a.style.left="0",a.style.right="0",a.style.justifyContent="center",a.style.alignItems="center",a.style.zIndex="99999",document.body.append(a)),a.innerHTML="";const n=document.createElement("div");n.style.color="white",n.style.background="#171a21",n.style.padding="1rem",n.innerHTML=e,a.appendChild(n),a.style.display="flex",a.onclick=()=>{a.removeChild(n),a.style.display="none"}}window.__maistho_csgo_stats_compare=i=>{const r={selfScore:0,otherScore:0,selfKills:0,otherKills:0,selfKdr:0,otherKdr:0};rawMatches.filter(e=>e.teams.some(e=>e.some(e=>e.link===i))).forEach(e=>{let t,a;for(const s of e.teams)for(const o of s)if(o.link===i&&(a=o),(t=o.link===profileLink?o:t)&&a)break;var n;t.score>a.score?r.selfScore++:t.score<a.score&&r.otherScore++,t.kills>a.kills?r.selfKills++:t.kills<a.kills&&r.otherKills++,t.deaths&&a.deaths&&(e=t.kills/t.deaths,(n=a.kills/a.deaths)<e?r.selfKdr++:e<n&&r.otherKdr++)}),showModal(`
<p>You get a higher score ${fmt(r.selfScore/(r.selfScore+r.otherScore)*100,0)}% of the time!
</p>
<p>
You get a higher number of kills ${fmt(r.selfKills/(r.selfKills+r.otherKills)*100,0)}% of the time!
</p>
<p>
You get a higher KDR ${fmt(r.selfKdr/(r.selfKdr+r.otherKdr)*100,0)}% of the time!
</p>
`),console.log(r)},getAllMatches().then(e=>{rawMatches=e;var{maps:t,players:a}=parsedData=parseMatches(e);printStatistics(t,Object.values(a),e)}).catch(e=>{console.warn(e),alert(e)}).finally(()=>{document.body.removeChild(loadingSpinner)});
Changelog
- July 23rd: Added assists and deaths to player table, improved caching. Fixed a bug with Firefox
How it works
I’ve written a different tool earlier which can dump all of your match data to JSON, available at npm/csgo-player-stats
I didn’t like how much of a hassle it was to extract the cookie and thought “Hey, this could be a bookmarklet!”
Everything can be a bookmarklet it you
wasteinvest enough hours on it
Since the original was written in Typescript, so is this bookmarklet. It adds some nifty features, such as comparing stats with your friends.
I’ve also added some features to this blog in order to be able to develop bookmarklet in typescript and transpile/minify it easier. I’ll try to get a blog post on that up later.
I’m pulling the data from Steams GDPR-takeout page: https://steamcommunity.com/my/gcpd/730?tab=matchhistorycompetitive where you can see all competitive matches (?) from the end of 2017 and forwards.
Full code available below:
enum CsgoMap {
de_mirage = 'Mirage',
de_dust2 = 'Dust II',
de_subzero = 'Subzero',
de_train = 'Train',
de_nuke = 'Nuke',
de_inferno = 'Inferno',
de_cache = 'Cache',
de_overpass = 'Overpass',
}
interface Match {
map: CsgoMap
time: Date
waitTime: string
matchDuration: string
score: string
demo?: string
teams: Team[]
userTeam: number
}
type Team = Player[]
interface Player {
link: string
image: string
name: string
ping: number
kills: number
assists: number
deaths: number
mvps: number
headshotRate: number
score: number
}
class MinMaxAvg {
min: number = Infinity
max: number = -Infinity
avg: number = 0
total: number = 0
append(n: number, totalTimes: number) {
this.min = Math.min(n, this.min)
this.max = Math.max(n, this.max)
this.total += n
this.avg = this.total / totalTimes
}
}
class PlayerStats {
link: string
image: string
name: string
constructor(player: Player) {
this.link = player.link
this.image = player.image
this.name = player.name
}
timesPlayed: number = 0
roundsPlayed: number = 0
ping = new MinMaxAvg()
kills = new MinMaxAvg()
assists = new MinMaxAvg()
deaths = new MinMaxAvg()
mvps = new MinMaxAvg()
headshotRate = new MinMaxAvg()
score = new MinMaxAvg()
add(player: Player, rounds: number) {
this.roundsPlayed += rounds
this.timesPlayed += 1
this.ping.append(player.ping, this.timesPlayed)
this.kills.append(player.kills, this.timesPlayed)
this.assists.append(player.assists, this.timesPlayed)
this.deaths.append(player.deaths, this.timesPlayed)
this.mvps.append(player.mvps, this.timesPlayed)
this.headshotRate.append(Number(player.headshotRate), this.timesPlayed)
this.score.append(player.score, this.timesPlayed)
}
}
const CACHE_KEY = '__maistho_csgo_stats_cached_matches'
function getCachedMatches(): Match[] {
try {
const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || '')
cached.forEach(match => (match.time = new Date(match.time)))
console.log(cached)
return cached
} catch (err) {
localStorage.removeItem(CACHE_KEY)
return []
}
}
async function fetchWithRetry(
input: RequestInfo,
init?: RequestInit,
): Promise<Response> {
for (let i = 0; i < 5; ++i) {
try {
return fetch(input, init)
} catch (err) {
console.warn(err)
await new Promise(resolve => setTimeout(resolve, i * 500))
}
}
}
let profileLink
async function getAllMatches() {
if (window.location.hostname != 'steamcommunity.com') {
window.location.href =
'https://steamcommunity.com/my/gcpd/730?tab=matchhistorycompetitive'
throw new Error(
'Redirecting to steamcommunity.com, please run the bookmarklet again after the page has reloaded',
)
}
const userAvatar: HTMLAnchorElement | null = document.querySelector(
'#global_actions>a.user_avatar',
)
if (!userAvatar) {
throw new Error('You need to be logged in')
}
profileLink = userAvatar.href.replace(/\/$/, '')
const cachedMatches = getCachedMatches()
const matchIds = cachedMatches.reduce((ids, match) => {
ids[match.time.toISOString()] = true
return ids
}, {})
const matches: Match[] = []
let continueToken: string | undefined
let i = 0
let foundCached = false
do {
const params = new URLSearchParams({
ajax: '1',
tab: 'matchhistorycompetitive',
})
if (continueToken) {
params.append('continue_token', continueToken)
}
const url = `${profileLink}/gcpd/730?${params.toString()}`
const response = await fetchWithRetry(url, {
credentials: 'include',
}).then(res => res.json())
if (!response.success) {
throw new Error(JSON.stringify(response))
}
const parser = new DOMParser()
const dom = parser.parseFromString(response.html, 'text/html')
for (const tr of Array.from(
dom.querySelectorAll('.generic_kv_table.csgo_scoreboard_root>tbody>tr'),
)) {
const match = {
map: getMap(tr),
time: getTime(tr),
waitTime: getWaitTime(tr),
matchDuration: getMatchDuration(tr),
score: getScore(tr),
demo: getDemo(tr),
...getTeams(tr, profileLink),
}
if (match.map) {
if (matchIds[match.time.toISOString()]) {
foundCached = true
break
}
matches.push(match)
}
}
continueToken = response.continue_token
loadingStatus.innerText = `${++i} pages parsed...`
} while (!foundCached && continueToken)
const result = [...matches, ...cachedMatches]
localStorage.setItem(CACHE_KEY, JSON.stringify(result))
return result
}
const getTeams = (tr: Element, profileLink: string) => {
const teams: Team[] = [[], []]
const players = Array.from(
tr.querySelectorAll('.csgo_scoreboard_inner_right>tbody>tr'),
)
let currentTeam = -1
for (const player of players) {
const userLink = q(player, 'a.linkTitle')
if (userLink == null) {
currentTeam += 1
continue
}
teams[currentTeam].push({
link: userLink.attributes.getNamedItem('href')!.textContent || '',
name: getInfo(player, 'a.linkTitle'),
image: getAttribute(q(player, '.playerAvatar img'), 'src'),
ping: parseInt(getInfo(player, 'td:nth-child(2)')),
kills: parseInt(getInfo(player, 'td:nth-child(3)')),
assists: parseInt(getInfo(player, 'td:nth-child(4)')),
deaths: parseInt(getInfo(player, 'td:nth-child(5)')),
mvps: parseInt(getInfo(player, 'td:nth-child(6)').substr(1)) || 0,
headshotRate:
Number(getInfo(player, 'td:nth-child(7)').replace(/%$/, '')) / 100,
score: parseInt(getInfo(player, 'td:nth-child(8)')),
})
}
const userTeam = teams.findIndex(team =>
team.some(player => player.link === profileLink),
)
return { teams, userTeam }
}
const getDemo = (tr: Element | null) => {
const text = q(tr, '.csgo_scoreboard_inner_left .csgo_scoreboard_btn_gotv')
const button = text && text.parentElement
return getAttribute(button, 'href')
}
const getScore = (tr: Element | null) => getInfo(tr, '.csgo_scoreboard_score')
const getMatchDuration = (tr: Element | null) =>
getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(4)').replace(
/^Match Duration: /,
'',
)
const getWaitTime = (tr: Element | null) =>
getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(3)').replace(
/^Wait Time: /,
'',
)
const getTime = (tr: Element | null) =>
new Date(
getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(2)').replace(
/\s*GMT$/i,
'',
),
)
const getMap = (tr: Element | null) =>
getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(1)').replace(
/^Competitive /,
'',
) as CsgoMap
function getInfo(el: Element | null, query: string): string {
return getText(q(el, query))
}
function getText(el: Element | null): string {
return ((el && el.textContent) || '').trim()
}
function q(el: Element | null, query: string): Element | null {
return el && el.querySelector(query)
}
function getAttribute(el: Element | null, attribute: string) {
return ((el && el.getAttribute(attribute)) || '').trim()
}
type MapData = {
[key: string]: {
name: string
timesPlayed: number
wins: number
losses: number
draws: number
winrate: number
roundsWon: number
roundsLost: number
roundWinrate: number
}
}
function parseMatches(data: Match[]) {
const maps: MapData = {}
const players: Record<string, PlayerStats> = {}
data.forEach((match: Match) => {
const score = match.score
.split(':')
.map(s => s.trim())
.map(s => Number(s))
const rounds = score[0] + score[1]
const myTeam = match.userTeam
const notMyTeam = myTeam === 0 ? 1 : 0
if (!maps[match.map]) {
maps[match.map] = {
name: match.map,
timesPlayed: 0,
wins: 0,
losses: 0,
draws: 0,
winrate: 0,
roundsWon: 0,
roundsLost: 0,
roundWinrate: 0,
}
}
const map = maps[match.map]
if (score[myTeam] > score[notMyTeam]) {
map.wins += 1
} else if (score[myTeam] < score[notMyTeam]) {
map.losses += 1
} else {
map.draws += 1
}
map.timesPlayed += 1
map.roundsWon += score[myTeam]
map.roundsLost += score[notMyTeam]
for (const team of match.teams) {
for (const player of team) {
if (!players[player.link]) {
players[player.link] = new PlayerStats(player)
}
const stats = players[player.link]
stats.add(player, rounds)
}
}
})
Object.keys(maps).forEach(name => {
const map = maps[name]
map.winrate = map.wins / (map.timesPlayed - map.draws)
map.roundWinrate = map.roundsWon / (map.roundsWon + map.roundsLost)
})
return { maps, players }
}
function fmt(n: number, precision: number = 2) {
return Number(n.toFixed(precision))
}
function displayPlayer(p: PlayerStats) {
return {
image: `<a href="${p.link}"><img src="${p.image}"></a>`,
name: `<a href="${p.link}">${p.name}</a>`,
gamesPlayed: p.timesPlayed,
kdr: fmt(p.kills.total / p.deaths.total),
totalKills: p.kills.total,
killsPerGame: fmt(p.kills.avg),
killsPerRound: fmt(p.kills.total / p.roundsPlayed),
totalAssists: p.assists.total,
assistsPerGame: fmt(p.assists.avg),
assistsPerRound: fmt(p.assists.total / p.roundsPlayed),
totalDeaths: p.deaths.total,
deathsPerGame: fmt(p.deaths.avg),
deathsPerRound: fmt(p.deaths.total / p.roundsPlayed),
headshots: fmt(p.headshotRate.avg),
compareScore: `<button onclick="window.__maistho_csgo_stats_compare('${p.link}');">Compare</button>`,
}
}
function printStatistics(
maps: MapData,
players: PlayerStats[],
matches: Match[],
) {
const topPlayers = players
.sort((b, a) => a.timesPlayed - b.timesPlayed)
.slice(0, 20)
var data = Object.values(maps)
.sort((a, b) => b.winrate - a.winrate)
.map(map => {
return {
...map,
winrate: formatWinrate(map.winrate),
roundWinrate: formatWinrate(map.roundWinrate),
}
})
console.table(topPlayers)
console.table(data)
const h2Style = `text-align: center; margin: 2em 0 0.5em 0;`
const sectionStyle = `overflow: auto;`
let res = `
<section style="${sectionStyle}">
<h2 style="${h2Style}">Map statistics</h2>
${makeTable(data)}
</section>
<section style="${sectionStyle}">
<h2 style="${h2Style}">Player statistics</h2>
${makeTable(topPlayers.map(displayPlayer))}
</section>
<section>
<h2>Most recent game</h2>
<p>${matches[0].time}</p>
<h2>Earliest game</h2>
<p>${matches[matches.length - 1].time}</p>
</section>
`
const ID = '__maistho_csgo_stats_table_wrapper'
let el = document.getElementById(ID)
if (!el) {
el = document.createElement('div')
el.id = ID
el.style.background = '#171a21'
el.style.color = 'white'
document.body.prepend(el)
}
el.innerHTML = res
const header: HTMLDivElement | null = document.querySelector(
'.responsive_header',
)
if (header) {
const height = `${el.clientHeight}px`
header.style.marginTop = height
}
}
function makeTable(data: any[]) {
const keys = Object.keys(data[0])
const tdStyle = `padding: 0.125rem 0.5rem;`
const headers = keys
.map(k => {
const str = `${k[0].toLocaleUpperCase()}${k.substr(1)}`.replace(
/([A-Z])/g,
' $1',
)
return `<th style="${tdStyle}">${str}</th>`
})
.join('\n')
const rows = data
.map((d, i) => {
const values = keys
.map(
k =>
`<td style="${tdStyle} ${
i % 2 === 0 ? 'background-color: rgba(255,255,255,0.2);' : ''
}">${d[k]}</td>`,
)
.join('\n')
return `<tr>${values}</tr>`
})
.join('\n')
return `<table style="margin: 0 auto; border-collapse: collapse;">
<thead>
<tr>
${headers}
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>`
}
function formatWinrate(winrate: number) {
return (
(winrate * 100).toLocaleString(undefined, {
maximumFractionDigits: 2,
minimumFractionDigits: 0,
}) + '%'
)
}
const loadingSpinner = document.createElement('div')
loadingSpinner.innerHTML = `<img src="https://steamcommunity-a.akamaihd.net/public/images/login/throbber.gif">`
loadingSpinner.style.display = 'flex'
loadingSpinner.style.alignItems = 'center'
loadingSpinner.style.justifyContent = 'center'
loadingSpinner.style.position = 'fixed'
loadingSpinner.style.top = '0'
loadingSpinner.style.left = '0'
loadingSpinner.style.bottom = '0'
loadingSpinner.style.right = '0'
loadingSpinner.style.background = 'rgba(0,0,0,0.5)'
loadingSpinner.style.flexDirection = 'column'
loadingSpinner.style.zIndex = '999999'
const loadingStatus = document.createElement('div')
loadingStatus.innerText = '0 pages parsed...'
loadingStatus.style.color = 'white'
loadingStatus.style.margin = '1em'
loadingSpinner.appendChild(loadingStatus)
document.body.appendChild(loadingSpinner)
let rawMatches: Match[]
let parsedData: { maps: MapData; players: Record<string, PlayerStats> }
;(window as any).__maistho_csgo_stats_compare = link => {
const stats = {
selfScore: 0,
otherScore: 0,
selfKills: 0,
otherKills: 0,
selfKdr: 0,
otherKdr: 0,
}
rawMatches
.filter(match => match.teams.some(t => t.some(p => p.link === link)))
.forEach(match => {
let self: Player
let other: Player
for (const team of match.teams) {
for (const p of team) {
if (p.link === link) {
other = p
}
if (p.link === profileLink) {
self = p
}
if (self && other) {
break
}
}
}
if (self.score > other.score) {
stats.selfScore++
} else if (self.score < other.score) {
stats.otherScore++
}
if (self.kills > other.kills) {
stats.selfKills++
} else if (self.kills < other.kills) {
stats.otherKills++
}
if (self.deaths && other.deaths) {
const selfKdr = self.kills / self.deaths
const otherKdr = other.kills / other.deaths
if (selfKdr > otherKdr) {
stats.selfKdr++
} else if (selfKdr < otherKdr) {
stats.otherKdr++
}
}
})
showModal(
`
<p>You get a higher score ${fmt(
(stats.selfScore / (stats.selfScore + stats.otherScore)) * 100,
0,
)}% of the time!
</p>
<p>
You get a higher number of kills ${fmt(
(stats.selfKills / (stats.selfKills + stats.otherKills)) * 100,
0,
)}% of the time!
</p>
<p>
You get a higher KDR ${fmt(
(stats.selfKdr / (stats.selfKdr + stats.otherKdr)) * 100,
0,
)}% of the time!
</p>
`,
)
console.log(stats)
}
getAllMatches()
.then(matches => {
rawMatches = matches
parsedData = parseMatches(matches)
const { maps, players } = parsedData
printStatistics(maps, Object.values(players), matches)
})
.catch(err => {
console.warn(err)
alert(err)
})
.finally(() => {
document.body.removeChild(loadingSpinner)
})
function showModal(content: string) {
const ID = '__maistho_csgo_stats_modal_wrapper'
let el = document.getElementById(ID)
if (!el) {
el = document.createElement('div')
el.id = ID
el.style.background = 'rgba(0,0,0,0.5)'
el.style.position = 'fixed'
el.style.top = '0'
el.style.bottom = '0'
el.style.left = '0'
el.style.right = '0'
el.style.justifyContent = 'center'
el.style.alignItems = 'center'
el.style.zIndex = '99999'
document.body.append(el)
}
el.innerHTML = ''
const modal = document.createElement('div')
modal.style.color = 'white'
modal.style.background = '#171a21'
modal.style.padding = '1rem'
modal.innerHTML = content
el.appendChild(modal)
el.style.display = 'flex'
el.onclick = () => {
el.removeChild(modal)
el.style.display = 'none'
}
}