Last active
August 21, 2024 03:57
-
-
Save nabbynz/0d56c5e8d9b7a2e19bafeeaf138413ae to your computer and use it in GitHub Desktop.
Show Replay Chat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Show Replay Chat | |
// @description Shows only the chat from a game replay | |
// @version 0.5.1 | |
// @match https://*.koalabeast.com/replays* | |
// @match https://*.koalabeast.com/game* | |
// @updateURL https://gist.github.com/nabbynz/0d56c5e8d9b7a2e19bafeeaf138413ae/raw/Show_Replay_Chat.user.js | |
// @downloadURL https://gist.github.com/nabbynz/0d56c5e8d9b7a2e19bafeeaf138413ae/raw/Show_Replay_Chat.user.js | |
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_deleteValue | |
// @grant GM_addStyle | |
// @author nabby | |
// @require https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js | |
// ==/UserScript== | |
'use strict'; | |
console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')'); | |
/* eslint-env jquery */ | |
/* globals tagpro, moment, dayjs */ | |
/* eslint-disable no-multi-spaces, no-useless-concat */ | |
const MINIMIZED_WIDTH = 300; // minimized width of chat log when in an actual replay (0 to not show) | |
const REMOVE_FROM_CACHE_AFTER = 3 * 24 * 60 * 60 * 1000; // remove cached games after 24 hours | |
const USE_LONG_SERVER_NAMES = true; // change "SWA" -> Seattle | |
const redTextColor = '#ffb5bd'; | |
const blueTextColor = '#cfcfff'; | |
const redBackgroundColor = '#a13636'; | |
const blueBackgroundColor = '#194773'; | |
let userId = $('#userId').val() || ''; | |
let lastGet = 0; | |
let cachedGames = GM_getValue('cachedGames', {}); | |
let removeOldCachedGames = function() { | |
let doSave = false; | |
for (let gameId in cachedGames) { | |
if (cachedGames[gameId].dateCached < Date.now() - REMOVE_FROM_CACHE_AFTER) { | |
delete cachedGames[gameId]; | |
doSave = true; | |
} | |
} | |
if (doSave) { | |
GM_setValue('cachedGames', cachedGames); | |
} | |
}; | |
removeOldCachedGames(); | |
let reformatServerName = function(server) { | |
let city = server.replace('Map Testing - ', '').replace(' - Test', '').trim(); | |
let isMapTest = server.includes('Map Testing - '); | |
let isTest = !isMapTest && server.includes(' - Test'); | |
if (server.indexOf(',') >= 0) { | |
city = server.slice(0, server.indexOf(',')); | |
} | |
return city + ((isMapTest || isTest) ? ' (' + (isMapTest ? 'MT' : '') + (isTest ? 'T' : '') + ')' : ''); | |
}; | |
let getReplayKey = function(gameId, userId, extra, host) { | |
return btoa((gameId + (userId || "")).match(/\w{2}/g).map((gameId=>String.fromCharCode(parseInt(gameId, 16)))).join("")).replaceAll("+", "_"); | |
}; | |
let getReplayUrl = function(gameId, userId, extra, host) { | |
return (host || "") + "/game?replay=" + getReplayKey(gameId, userId) + (extra || ""); | |
}; | |
let formatDate = function(started, asTitle, asCalendar) { | |
let date; | |
if (asTitle) { | |
date = moment(started).format("YYYY/MM/DD hh:mma"); | |
} else { | |
date = moment(started).calendar(null, { | |
sameDay: "[Today] hh:mma", | |
lastDay: "[Yesterday] hh:mma", | |
lastWeek: "dddd hh:mma", | |
sameElse: "YYYY/MM/DD hh:mma" | |
}); | |
} | |
return asCalendar ? date.split(" ") : date; | |
}; | |
let addShowChatLinks = function() { | |
let currentTab = $('ul.tab-list').find('li.active').attr('data-tab'); | |
let isCurrentTabAll = currentTab === 'all'; | |
let mapNameWidth = ($('.table-teams tbody tr').eq(1).find('td').eq(2).width() - 89) + 'px'; | |
$('.table-teams tbody tr').each(function(i) { | |
let gameHref = $(this).find('td').last().find('a[href*="/replays/"]').attr('href'); | |
let gameId = ''; | |
let replayHref = $(this).find('td').last().find('a[href*="/game?replay="]').attr('href'); | |
let replayId = ''; | |
gameId = gameHref.slice(gameHref.indexOf('gameId=') + 7); | |
gameId = gameId.slice(0, gameId.indexOf('&')); | |
replayId = replayHref.slice(replayHref.indexOf('?replay=') + 8); | |
$(this).find('td').eq(0).attr('data-sortby', i); // Start | |
$(this).find('td').eq(isCurrentTabAll ? 4 : 5).attr('data-sortby', toSeconds($(this).find('td').eq(isCurrentTabAll ? 4 : 5).text())); // Duration | |
$(this).find('td').last().append('<a href="/showchat?gameId=' + gameId + (userId ? '&userId=' + userId : '') + '" class="SRC_ShowChat" data-gameid="' + gameId + '" data-replayid="' + replayId + '" title="Show Chat"><i class="fa-solid fa-comment-dots' + (cachedGames.hasOwnProperty(gameId) ? ' SRC_Cached' : '') + '"></i></a>'); | |
// highlight overtime games... | |
let type = (isCurrentTabAll ? 'Public' : $(this).find('td').eq(4).text().trim()); | |
if (type === 'Public') { | |
let duration = toSeconds( $(this).find('td').eq(isCurrentTabAll ? 4 : 5).text().trim() ); | |
if (duration > 360) { | |
$(this).find('td').eq(isCurrentTabAll ? 4 : 5).addClass('SRC_Overtime').attr('title', 'Overtime'); | |
} | |
} | |
if (USE_LONG_SERVER_NAMES) { | |
let serverName = reformatServerName($(this).find('td').eq(currentTab === 'favorite' ? 5 : 3).attr('title')); | |
$(this).find('td').eq(currentTab === 'favorite' ? 5 : 3).text(serverName); | |
$(this).find('td').eq(2).css('max-width', mapNameWidth); | |
} | |
}); | |
}; | |
let checkTableTRs = function() { | |
if (!$('.table-teams').find('tbody tr').length && !$('ul.tab-list').length) { // rows haven't appeared yet... | |
return false; | |
} | |
return true; | |
}; | |
async function waitForTRs() { // waits for the table rows to appear | |
$('#SRC_GameStats_Container').hide(); | |
while (!checkTableTRs()) { | |
await new Promise(resolve => setTimeout(resolve, 200)); | |
} | |
if (checkTableTRs()) { | |
addShowChatLinks(); | |
} | |
} | |
let addMainContainer = function() { | |
$('#replays-app').append('<div id="SRC_ChatLog"><div id="SRC_ChatLog_Header"><div id="SRC_CapTimeline_Container"></div><span id="SRC_ChatLog_Close" title="Close">X</span></div><div id="SRC_ChatLog_Chat"></div></div>'); | |
}; | |
let jsonData = []; | |
let getGamesData = function(callback) { | |
const URL = '/replays/data'; | |
const pages = ['?page=0&pageSize=50', '?page=1&pageSize=50', '?page=2&pageSize=50', '?page=3&pageSize=50', '?page=4&pageSize=50']; | |
const getGames = page => fetch(`${URL}/${page}`).then(response => response.json() ); | |
const arrayOfPromises = pages.map(page => getGames(page)); | |
const allPromisesWithErrorHandler = arrayOfPromises.map(promise => promise.catch(error => error), ); | |
Promise.all(allPromisesWithErrorHandler).then(data => { | |
jsonData = []; | |
for (let i in data) { | |
jsonData = jsonData.concat(data[i].games); | |
} | |
gameStatsLoadedDate = Date.now(); | |
callback(true); | |
}); | |
}; | |
let addExtraRowsToTable = function() { | |
let row = 1; | |
$('.table-teams tbody').empty(); | |
for (let game of jsonData) { | |
let fDate = formatDate(game.started, false, true); | |
let fDateTitle = formatDate(game.started, true, false); | |
let replayKey = getReplayKey(game.id, userId); | |
let replayUrl = getReplayUrl(game.id, userId, null, game.redirectHost); | |
let favorite = game.favorite; | |
$('.table-teams tbody').append('<tr>' + | |
' <td class="nogrow" data-sortby="' + row + '" title="' + fDateTitle + '"><span class="SRC_RowNumber">[' + row + ']</span>' + fDate[0] + '</td>' + | |
' <td class="nogrow" title="' + fDateTitle + '">' + fDate[1] + '</td>' + | |
' <td>' + game.mapName + '</td>' + | |
' <td class="nogrow">' + reformatServerName(game.server) + '</td>' + | |
' <td class="nogrow' + (Math.floor(game.duration / 1000) > 360 ? ' SRC_Overtime' : '') + '" data-sortby="' + game.duration + '">' + tagpro.helpers.timeFromSeconds(Math.floor(game.duration / 1000), true) + '</td>' + | |
' <td class="nogrow ' + (game.myTeam && userId ? game.myTeam + '-team-text' : '') + '">' + (game.myTeam && userId ? game.myTeamName : '') + '</td>' + | |
' <td class="nogrow ' + (game.myTeam && userId ? game.myTeam + '-team-text' : '') + '">' + (game.myTeamWon ? '<i title="Winner" class="fa fa-trophy"></i>' : '') + '</td>' + | |
' <td class="nogrow text-center ' + (game.winner === 1 ? 'red-team' : 'red-team-text') + '">' + game.teams.red.score + '</td>' + | |
' <td class="nogrow text-center ' + (game.winner === 2 ? 'blue-team' : 'blue-team-text') + '">' + game.teams.blue.score + '</td>' + | |
' <td class="nogrow">' + | |
' <a href="' + replayUrl + '" target="replay"><i title="Play" class="fa fa-circle-play clickableText"></i></a>' + | |
' <a href="https://tagpro.eu/?match=' + game.uuid + '/" target="tagproeu"><i title="Game Info on tagpro.eu" class="fa fa-circle-info clickableText"></i></a>' + | |
(userId ? ' <a href="replays/gameFile?gameId=' + game.id + '&userid=' + userId + '/"><i title="Download" class="fa fa-circle-down clickableText"></i></a>' : '') + | |
(userId ? ' <i title="' + (game.favorite ? 'Remove from Favorites' : 'Add to Favorites') + '" class="fa-heart clickableText ' + (game.favorite ? 'fa' : 'fa-regular') + ' SRC_Favorite" data-key="' + replayKey + '"></i>' : '') + | |
'<a href="/showchat?gameId=' + game.id + (userId ? '&userId=' + userId : '') + '" class="SRC_ShowChat" data-gameid="' + game.id + '" data-replayid="' + replayKey + '" title="Show Chat"><i class="fa-solid fa-comment-dots' + (cachedGames.hasOwnProperty(game.id) ? ' SRC_Cached' : '') + '"></i></a>' + | |
' </td>' + | |
'</tr>'); | |
row++; | |
} | |
$('.table-teams thead th').css('text-decoration', 'none'); | |
$('.table-teams').next('div').remove(); | |
}; | |
let gameStatsCached = null; | |
let gameStatsLoadedDate = null; | |
let initGameStatsNeeded = true; | |
let initGameStats = function() { | |
GM_addStyle('#SRC_GameStats_Container { position:fixed; display:flex; flex-flow:row wrap; width:1150px; justify-content:space-between; top:50%; left:50%; padding:10px; font-size:13px; transform:translate(-50%, -50%); text-align:center; color:#ccc; background:#0a0a0a; border:2px ridge #9566df; border-radius:6px; box-shadow:0px 0px 25px 10px black; z-index:999; }'); | |
GM_addStyle('#SRC_GameStats_Reload { position:absolute; width:17px; height:17px; bottom:3px; right:3px; padding:0 0 0 1px; color:lightslategray; border:1px solid darkslateblue; font-size:12px; font-weight:bold; border-radius:10px; cursor:pointer; }'); | |
GM_addStyle('#SRC_GameStats_Close { position:absolute; width:17px; height:17px; top:3px; right:2px; padding:0 0 0 1px; color:red; border:1px solid darkred; font-size:12px; font-weight:bold; border-radius:10px; cursor:pointer; }'); | |
GM_addStyle('#SRC_GameStats_Header { width:fit-content; font-size:12px; background:#3f3f3f; height:40px; border-bottom:1px solid #444; }'); | |
GM_addStyle('#SRC_GameStats_Table { width:fit-content; text-align:center; max-height:220px; overflow-y:scroll; display:block; }'); | |
GM_addStyle('#SRC_ServerStats_Header { width:fit-content; margin:20px auto 0; font-size:12px; background:#3f3f3f; height:40px; border-bottom:1px solid #444; }'); | |
GM_addStyle('#SRC_ServerStats_Table { width:fit-content; margin:0px auto 20px; text-align:center; max-height:220px; overflow-y:scroll; display:block; }'); | |
GM_addStyle('#SRC_GameStats_Table tr, #SRC_ServerStats_Table tr { background:#2a2a2a; }'); | |
GM_addStyle('#SRC_GameStats_Table tr:nth-child(2n), #SRC_ServerStats_Table tr:nth-child(2n) { background:#212121; }'); | |
GM_addStyle('#SRC_GameStats_Table tbody tr:hover, #SRC_ServerStats_Table tbody tr:hover { background:#482d4e !important; }'); | |
GM_addStyle('#SRC_GameStats_Table tr td, #SRC_GameStats_Header tr th, #SRC_ServerStats_Table tr td, #SRC_ServerStats_Header tr th { padding:1px 5px; }'); | |
GM_addStyle('#SRC_GameStats_Table tr td:nth-child(1), #SRC_GameStats_Header tr th:nth-child(1), #SRC_ServerStats_Table tr td:nth-child(1), #SRC_ServerStats_Header tr th:nth-child(1) { width:260px; max-width:260px; text-align:left; text-overflow:ellipsis; overflow:hidden; white-space:nowrap; }'); | |
GM_addStyle('#SRC_GameStats_Table tr td:nth-child(n+2), #SRC_GameStats_Header tr th:nth-child(n+2), #SRC_ServerStats_Table tr td:nth-child(n+2), #SRC_ServerStats_Header tr th:nth-child(n+2) { width:80px; max-width:80px; }'); | |
GM_addStyle('#SRC_GameStats_Table::-webkit-scrollbar, #SRC_ServerStats_Table::-webkit-scrollbar { width:3px; }'); | |
GM_addStyle('#SRC_GameStats_Table::-webkit-scrollbar-thumb, #SRC_ServerStats_Table::-webkit-scrollbar-thumb { background:#9566df; }'); | |
GM_addStyle('#SRC_GameStats_Table::-webkit-scrollbar-track, #SRC_ServerStats_Table::-webkit-scrollbar-track { background:#555; }'); | |
GM_addStyle('#SRC_Extra_Info { margin:20px auto; padding:10px 40px; border:1px outset #797979; border-radius:8px; height:fit-content; background:#222; }'); | |
GM_addStyle('#SRC_Extra_Info_Table { width:250px; }'); | |
GM_addStyle('#SRC_Extra_Info_Table td:nth-child(1) { width:50%; padding:0 6px; text-align:right; }'); | |
GM_addStyle('#SRC_Extra_Info_Table td:nth-child(2) { width:50%; padding:0 6px; text-align:left; }'); | |
GM_addStyle('#SRC_GameStats_Graph_Container { position:relative; margin:10px; padding:10px; background:#000; border:1px outset #888; border-radius:8px; }'); | |
GM_addStyle('#SRC_GameStats_Graph_ToolTip { position:absolute; display:none; padding:10px 40px; font-size:12px; color:#aaa; background:#222; border:1px outset #777; border-radius:10px; box-shadow:3px 3px 6px black; white-space:nowrap; }'); | |
GM_addStyle('.SRC_GameStats_Graph_LabelX { font-size:10px; font-weight:bold; fill:#ccc; }'); | |
GM_addStyle('.SRC_GameStats_Graph_LabelY { font-size:10px; font-weight:bold; fill:#ccc; }'); | |
GM_addStyle('.SRC_GameStats_Graph_MyTeam { stroke:#00ff00; stroke-width:2; opacity:0.4; }'); | |
GM_addStyle('.SRC_GameStats_Graph_Duration_Red { stroke:rgb(255,121,121,0.65); stroke-width:1; filter:drop-shadow(0px -1px 4px #e00) drop-shadow(0px -1px 2px #e00); }'); | |
GM_addStyle('.SRC_GameStats_Graph_Duration_Blue { stroke:rgba(141,171,255,0.65); stroke-width:1; filter:drop-shadow(0px 1px 4px #09f) drop-shadow(0px 1px 2px #09f); }'); | |
GM_addStyle('.SRC_GameStats_Graph_Duration_Abandoned { stroke:#ffffff; stroke-width:1; opacity:0.4; }'); | |
GM_addStyle('.SRC_GameStats_Graph_GameStart_Red { stroke:rgb(163 61 61); }'); | |
GM_addStyle('.SRC_GameStats_Graph_GameStart_Blue { stroke:rgb(90 113 144); }'); | |
GM_addStyle('.SRC_GameStats_Graph_GameStart_Abandoned { stroke:#ffffff; opacity:0.4; }'); | |
GM_addStyle('.SRC_GameStats_Graph_Guide { stroke:#444; stroke-width:1; stroke-dasharray:4 6; opacity:1; }'); | |
$('#replays-app').append('<div id="SRC_GameStats_Container"><span id="SRC_GameStats_Reload" title="Reload Data">R</span><span id="SRC_GameStats_Close" title="Close">X</span></div>'); | |
$('#SRC_GameStats_Container').hide(); | |
$('#SRC_GameStats_Container').on('click', '.SRC_GameStats_Sort', function() { | |
const target = this.dataset.target; | |
const index = this.cellIndex; | |
let sortasc = false; | |
if (!this.dataset.sortasc || this.dataset.sortasc === undefined || this.dataset.sortasc === 'false') { | |
this.dataset.sortasc = 'true'; | |
sortasc = true; | |
} else { | |
this.dataset.sortasc = 'false'; | |
sortasc = false; | |
} | |
$('#' + target + ' tbody tr').sort(function(a, b) { | |
if (index === 0) { | |
if (sortasc) { | |
return a.children[index].innerText.localeCompare(b.children[index].innerText); | |
} else { | |
return b.children[index].innerText.localeCompare(a.children[index].innerText); | |
} | |
} else { | |
if (sortasc) { | |
return a.children[index].dataset.sortby - b.children[index].dataset.sortby; | |
} else { | |
return b.children[index].dataset.sortby - a.children[index].dataset.sortby; | |
} | |
} | |
}).appendTo( $('#' + target) ); | |
$('#' + target).prev('table').find('.SRC_GameStats_Sort').css('text-decoration', 'none'); | |
$(this).css('text-decoration', 'underline'); | |
}); | |
$('#SRC_GameStats_Reload').on('click', function() { | |
$('#SRC_GameStats_Container').fadeOut(50, function() { | |
getGamesData(function(success) { | |
if (success) { | |
showGamesStats(); | |
} | |
}); | |
}); | |
}); | |
$('#SRC_GameStats_Close').on('click', function() { | |
$('#SRC_GameStats_Container').fadeOut(100); | |
}); | |
$('#SRC_GameStats_Container').on('click', '.SRC_GameStats_Graph_Details', function() { | |
let gameId = this.dataset.gameid; | |
//let replayId = this.dataset.replayid; | |
$('#SRC_GameStats_Container').prepend( $('#SRC_ChatLog') ); | |
if (cachedGames[gameId]) { | |
showChatLog(gameId, true); | |
} else { | |
if (!gameId || gameId.length !== 24) { | |
return; | |
} | |
getProcessAndShowReplay(gameId, true); | |
} | |
}); | |
$('#SRC_GameStats_Container').on('mouseover', '.SRC_GameStats_Graph_Details', function(e) { | |
$('#SRC_GameStats_Graph_ToolTip').html('<div>' + e.target.dataset.name + '</div><div>' + e.target.dataset.date + '</div><div>Duration: ' + e.target.dataset.duration + '</div><div>' + (e.target.dataset.winner === '1' ? 'Red Won ' : e.target.dataset.winner === '2' ? 'Blue Won ' : e.target.dataset.winner === '-1' ? 'Abandoned: ' : '') + e.target.dataset.score + '</div>' + (e.target.dataset.myteamname ? '<div>My Team: ' + e.target.dataset.myteamname + '</div>' : '')); | |
$('#SRC_GameStats_Graph_ToolTip').css({ 'left':e.offsetX - 100, 'top':e.clientY - 215 }).show(); | |
}); | |
$('#SRC_GameStats_Container').on('mouseleave', '.SRC_GameStats_Graph_Details', function() { | |
$('#SRC_GameStats_Graph_ToolTip').hide(); | |
}); | |
$('#SRC_GameStats_Container').on('click', '.SRC_GameStats_ShowMapGames', function() { | |
let wasSelected = this.dataset.selected; | |
$('.SRC_GameStats_ShowMapGames').css('color', 'inherit').attr('data-selected', ''); | |
if (wasSelected) { | |
$('#SRC_GameStats_Graph_Container').find('.SRC_GameStats_Graph_Details').fadeIn(50); | |
} else { | |
$(this).css('color', '#a27ed3'); | |
this.dataset.selected = 'selected'; | |
$('#SRC_GameStats_Graph_Container').find('.SRC_GameStats_Graph_Details').hide(); | |
$('#SRC_GameStats_Graph_Container').find('.SRC_GameStats_Graph_Details[data-map="' + this.dataset.sortby + '"]').fadeIn(50); | |
} | |
}); | |
}; | |
let makeFavorite = function(name, redWins, blueWins) { | |
let text = ''; | |
let sortby = 0; | |
let title = ''; | |
let total = redWins + blueWins; | |
if (total <= 2) { | |
sortby = 0; | |
text = '-'; | |
title = 'Not enough games' + ' (Red: ' + redWins + ', Blue: ' + blueWins + ')'; | |
} else if (redWins > blueWins) { | |
sortby = redWins / total * 100; | |
text = 'Red'; | |
title = 'Red Wins ' + sortby.toFixed(1) + '% of Games on ' + name + ' (Red: ' + redWins + ', Blue: ' + blueWins + ')'; | |
} else if (redWins < blueWins) { | |
sortby = blueWins / total * 100; | |
text = 'Blue'; | |
title = 'Blue Wins ' + sortby.toFixed(1) + '% of Games on ' + name + ' (Red: ' + redWins + ', Blue: ' + blueWins + ')'; | |
} else { | |
sortby = 50; | |
text = '='; | |
title = 'Even (Red: ' + redWins + ', Blue: ' + blueWins + ')'; | |
} | |
return { text: text, sortby: sortby, title: title }; | |
}; | |
let showGamesStats = function() { | |
let allData = { maps: {}, servers: {}, serversGraph: [], serverGamesCount:{} }; | |
let currentTab = $('ul.tab-list').find('li.active').attr('data-tab'); | |
let isCurrentTabAll = currentTab === 'all'; | |
$('#SRC_GameStats_Container').children().not('#SRC_GameStats_Close, #SRC_GameStats_Reload').remove(); | |
$('#SRC_GameStats_Container').append('<div><table id="SRC_GameStats_Header">' + | |
' <tr>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="Map Name">Map</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="Total Number of Games" data-sortasc="false" style="text-decoration:underline;">Games</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="Average Game Duration">Duration</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="Caps Per Minute">CPM</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="Average Red Score">Red Score</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="Average Blue Score">Blue Score</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_GameStats_Table" title="The Team That Wins Most">Favored</th>' + | |
' <th style="padding:0; width:3px;"></th>' + | |
' </tr>' + | |
'</table>' + | |
'<table id="SRC_GameStats_Table"></table></div>'); | |
$('#SRC_GameStats_Container').append('<div id="SRC_Extra_Info"></div>'); | |
$('#SRC_GameStats_Container').append('<div><table id="SRC_ServerStats_Header">' + | |
' <tr>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Average Game Duration">Server</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Total Number of Games" data-sortasc="false" style="text-decoration:underline;">Games</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Average Game Duration">Duration</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Caps Per Minute">CPM</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Average Red Score">Red Score</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Average Blue Score">Blue Score</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="The Team That Wins Most">Favored</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="Total Game Duration per Hour">Load %</th>' + | |
' <th class="SRC_GameStats_Sort" data-target="SRC_ServerStats_Table" title="# Games Started per Hour">Rate</th>' + | |
' <th style="padding:0; width:3px;"></th>' + | |
' </tr>' + | |
'</table>' + | |
'<table id="SRC_ServerStats_Table"></table></div>'); | |
let firstGameStarted, lastGameStarted; | |
let doOnce = false; | |
for (let game of jsonData) { | |
let mapName = game.mapName; | |
let serverName = game.server; | |
// create data for `maps` | |
if (game.winner !== -1) { // abandoned game - happens when all players quit a game? | |
if (!allData.maps.hasOwnProperty(mapName)) { | |
allData.maps[mapName] = { count: 0, wins: 0, duration: 0, redWins: 0, blueWins: 0, redTeamScore: 0, blueTeamScore: 0 }; | |
} | |
allData.maps[mapName].count++; | |
allData.maps[mapName].duration += game.duration; | |
allData.maps[mapName].redTeamScore += game.teams.red.score; | |
allData.maps[mapName].blueTeamScore += game.teams.blue.score; | |
allData.maps[mapName].redWins += game.winner === 1 ? 1 : 0; | |
allData.maps[mapName].blueWins += game.winner === 2 ? 1 : 0; | |
} | |
// create data for `servers` | |
if (!allData.servers.hasOwnProperty(serverName)) { | |
allData.servers[serverName] = { count: 0, wins: 0, duration: 0, redWins: 0, blueWins: 0, redTeamScore: 0, blueTeamScore: 0, margin1: 0, margin2: 0, overtime: 0, mercy: 0, abandoned: 0 }; | |
allData.serverGamesCount[serverName] = 0; | |
} | |
if (game.winner !== -1) { | |
allData.servers[serverName].count++; | |
allData.servers[serverName].duration += game.duration; | |
allData.servers[serverName].redTeamScore += game.teams.red.score; | |
allData.servers[serverName].blueTeamScore += game.teams.blue.score; | |
if (game.winner === 1) allData.servers[serverName].redWins++; | |
else if (game.winner === 2) allData.servers[serverName].blueWins++; | |
if (Math.abs(game.teams.red.score - game.teams.blue.score) === 3) allData.servers[serverName].mercy++; | |
else if (Math.round(game.duration / 1000) > 360) allData.servers[serverName].overtime++; | |
else if (Math.abs(game.teams.red.score - game.teams.blue.score) === 2) allData.servers[serverName].margin2++; | |
else if (Math.abs(game.teams.red.score - game.teams.blue.score) === 1) allData.servers[serverName].margin1++; | |
else allData.servers[serverName].normal++; | |
} else { | |
allData.servers[serverName].abandoned++; | |
} | |
// create data for `servers graph` | |
allData.serverGamesCount[serverName]++; | |
allData.serversGraph.push({ gameId: game.id, map:mapName, started: new Date(game.started).getTime(), server: game.server, duration: Math.floor(game.duration / 1000), winner: game.winner, redTeamScore: game.teams.red.score, blueTeamScore: game.teams.blue.score, myTeam: game.myTeam, myTeamWon: game.myTeamWon, myTeamName: game.myTeamName }); | |
} | |
firstGameStarted = dayjs(jsonData[0].started); | |
lastGameStarted = dayjs(jsonData[jsonData.length - 1].started); | |
const timePeriodMs = Date.now() - allData.serversGraph[allData.serversGraph.length - 1].started; | |
const timePeriodMins = timePeriodMs / 1000 / 60; | |
let redWinsTotal = 0; | |
let blueWinsTotal = 0; | |
let mercyTotal = 0; | |
let margin1Total = 0; | |
let margin2Total = 0; | |
let overtimesTotal = 0; | |
for (let map in allData.maps) { | |
redWinsTotal += allData.maps[map].redWins; | |
blueWinsTotal += allData.maps[map].blueWins; | |
let favorite = makeFavorite(map, allData.maps[map].redWins, allData.maps[map].blueWins); | |
$('#SRC_GameStats_Table').append('<tr>' + | |
' <td data-sortby="' + map + '" class="SRC_GameStats_ShowMapGames">' + map + '</td>' + | |
' <td data-sortby="' + allData.maps[map].count + '">' + allData.maps[map].count + '</td>' + | |
' <td data-sortby="' + allData.maps[map].duration / 1000 / allData.maps[map].count + '">' + tagpro.helpers.timeFromSeconds(Math.floor(allData.maps[map].duration / 1000 / allData.maps[map].count), true) + '</td>' + | |
' <td data-sortby="' + (allData.maps[map].redTeamScore + allData.maps[map].blueTeamScore) / (allData.maps[map].duration / 1000 / 60) + '">' + ((allData.maps[map].redTeamScore + allData.maps[map].blueTeamScore) / (allData.maps[map].duration / 1000 / 60)).toFixed(2) + '</td>' + | |
' <td data-sortby="' + allData.maps[map].redTeamScore / allData.maps[map].count + '">' + (allData.maps[map].redTeamScore / allData.maps[map].count).toFixed(1) + '</td>' + | |
' <td data-sortby="' + allData.maps[map].blueTeamScore / allData.maps[map].count + '">' + (allData.maps[map].blueTeamScore / allData.maps[map].count).toFixed(1) + '</td>' + | |
' <td data-sortby="' + favorite.sortby + '" title="' + favorite.title + '">' + favorite.text + '</td>' + | |
'</tr>'); | |
} | |
for (let server in allData.servers) { | |
let favorite = makeFavorite(server, allData.servers[server].redWins, allData.servers[server].blueWins); | |
let countTitle = 'Includes:\n'; | |
if (allData.servers[server].mercy) countTitle += ' · ' + allData.servers[server].mercy + 'x Wins by Mercy\n'; | |
if (allData.servers[server].margin1) countTitle += ' · ' + allData.servers[server].margin1 + 'x Wins by 1 Cap\n'; | |
if (allData.servers[server].margin2) countTitle += ' · ' + allData.servers[server].margin2 + 'x Wins by 2 Caps\n'; | |
if (allData.servers[server].overtime) countTitle += ' · ' + allData.servers[server].overtime + 'x Overtime Games\n'; | |
if (allData.servers[server].abandoned) countTitle += 'Excludes:\n · ' + allData.servers[server].abandoned + 'x Abandoned Games\n'; | |
mercyTotal += allData.servers[server].mercy; | |
margin1Total += allData.servers[server].margin1; | |
margin2Total += allData.servers[server].margin2; | |
overtimesTotal += allData.servers[server].overtime; | |
if (allData.servers[server].count) { | |
$('#SRC_ServerStats_Table').append('<tr>' + | |
' <td data-sortby="' + server + '">' + server + '</td>' + | |
' <td data-sortby="' + allData.servers[server].count + '"' + (countTitle ? ' title="' + countTitle + '"' : '') + '>' + allData.servers[server].count + '</td>' + | |
' <td data-sortby="' + allData.servers[server].duration / 1000 / allData.servers[server].count + '">' + tagpro.helpers.timeFromSeconds(Math.floor(allData.servers[server].duration / 1000 / allData.servers[server].count), true) + '</td>' + | |
' <td data-sortby="' + (allData.servers[server].redTeamScore + allData.servers[server].blueTeamScore) / (allData.servers[server].duration / 1000 / 60)+ '">' + ((allData.servers[server].redTeamScore + allData.servers[server].blueTeamScore) / (allData.servers[server].duration / 1000 / 60)).toFixed(2) + '</td>' + | |
' <td data-sortby="' + allData.servers[server].redTeamScore / allData.servers[server].count + '">' + (allData.servers[server].redTeamScore / allData.servers[server].count).toFixed(1) + '</td>' + | |
' <td data-sortby="' + allData.servers[server].blueTeamScore / allData.servers[server].count + '">' + (allData.servers[server].blueTeamScore / allData.servers[server].count).toFixed(1) + '</td>' + | |
' <td data-sortby="' + favorite.sortby + '" title="' + favorite.title + '">' + favorite.text + '</td>' + | |
' <td data-sortby="' + allData.servers[server].duration / timePeriodMs + '">' + (allData.servers[server].duration / timePeriodMs * 100).toFixed(1) + '%</td>' + | |
' <td data-sortby="' + allData.servers[server].count / timePeriodMins + '">' + (allData.servers[server].count / timePeriodMins * 60).toFixed(2) + '</td>' + | |
'</tr>'); | |
} | |
} | |
// console.log('SRC:: showGamesStats() allData:', allData); | |
let winsTotal = redWinsTotal + blueWinsTotal; | |
$('#SRC_Extra_Info').append('<div style="font-size:15px; font-weight:bold; margin:0 0 4px;">All Games' + ' (' + allData.serversGraph.length + ')</div>'); | |
$('#SRC_Extra_Info').append('<table id="SRC_Extra_Info_Table"></table>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Period</td><td>' + getYMDHOld(lastGameStarted, firstGameStarted).replace(' old', '') + '</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Game Rate</td><td>' + (allData.serversGraph.length / timePeriodMins * 60).toFixed(2) + ' per hour</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Mercy Wins</td><td>' + (mercyTotal / winsTotal * 100).toFixed(1) + '% (' + mercyTotal + ')</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>1-Cap Wins</td><td>' + (margin1Total / winsTotal * 100).toFixed(1) + '% (' + margin1Total + ')</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>2-Cap Wins</td><td>' + (margin2Total / winsTotal * 100).toFixed(1) + '% (' + margin2Total + ')</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Overtime Games</td><td>' + (overtimesTotal / winsTotal * 100).toFixed(1) + '% (' + overtimesTotal + ')</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Abandoned Games</td><td>' + ((allData.serversGraph.length - winsTotal) / winsTotal * 100).toFixed(1) + '% (' + (allData.serversGraph.length - winsTotal) + ')</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Red Wins</td><td>' + (redWinsTotal / winsTotal * 100).toFixed(1) + '% (' + redWinsTotal + ')</td></tr>'); | |
$('#SRC_Extra_Info_Table').append('<tr><td>Blue Wins</td><td>' + (blueWinsTotal / winsTotal * 100).toFixed(1) + '% (' + blueWinsTotal + ')</td></tr>'); | |
$('#SRC_GameStats_Container').append('<div id="SRC_GameStats_Graph_Container"></div>'); | |
$('#SRC_GameStats_Container').append('<div id="SRC_GameStats_Graph_ToolTip"></div>'); | |
showGameStatsActivityGraph(allData); | |
$('#SRC_GameStats_Table tbody tr').sort(function(a, b) { | |
return b.children[1].dataset.sortby - a.children[1].dataset.sortby; | |
}).appendTo( $('#SRC_GameStats_Table') ); | |
$('#SRC_ServerStats_Table tbody tr').sort(function(a, b) { | |
return b.children[1].dataset.sortby - a.children[1].dataset.sortby; | |
}).appendTo( $('#SRC_ServerStats_Table') ); | |
$('#SRC_GameStats_Container').fadeIn(200); | |
}; | |
const graphWidthTotal = 1080; | |
const graphPaddingLeft = 100; | |
const graphWidth = graphWidthTotal - graphPaddingLeft; | |
let showGameStatsActivityGraph = function(allData) { | |
const now = Date.now(); | |
const firstGameStarted = allData.serversGraph[allData.serversGraph.length - 1].started; | |
const timePeriodMs = now - firstGameStarted; | |
const timePeriodMins = timePeriodMs / 1000 / 60; | |
let serverCounts = []; | |
for (let server in allData.servers) { | |
serverCounts.push({ name:server, count:allData.servers[server].count }); | |
} | |
serverCounts.sort(function(a, b) { | |
return b.count - a.count; | |
}); | |
const graphHeight = (serverCounts.length + 1) * 20; | |
let svg = '<svg id="SRC_GameStats_Graph_SVG" width="' + graphWidthTotal + '" height="' + graphHeight + '" xmlns="http://www.w3.org/2000/svg" style="shape-rendering:crispEdges;">'; | |
let serverPositions = {}; | |
for (let i in serverCounts) { | |
serverPositions[serverCounts[i].name] = { count:serverCounts[i].count, position:(+i + 1) }; | |
svg += '<text x="' + (0 + 0) + '" y="' + ((+i + 1) * 20 - 7) + '" class="SRC_GameStats_Graph_LabelY">' + serverCounts[i].name + '</text>'; | |
} | |
for (let server of allData.serversGraph) { | |
let x1 = graphPaddingLeft + ((server.started - firstGameStarted) / timePeriodMs) * graphWidth; | |
let x2 = graphPaddingLeft + ((server.started - firstGameStarted + server.duration * 1000) / timePeriodMs) * graphWidth; | |
let y = serverPositions[server.server].position * 20 - 10; | |
svg += '<line x1="' + x1 + '" y1="' + y + '" x2="' + x2 + '" y2="' + y + '" class="SRC_GameStats_Graph_Details ' + (server.winner === 1 ? 'SRC_GameStats_Graph_Duration_Red' : server.winner === 2 ? 'SRC_GameStats_Graph_Duration_Blue' : 'SRC_GameStats_Graph_Duration_Abandoned') + '" data-gameid="' + server.gameId + '" data-map="' + server.map + '" data-name="' + server.server + '" data-date="' + dayjs(server.started).format('DD-MMM-YYYY h:mma') + '" data-duration="' + tagpro.helpers.timeFromSeconds(server.duration, true) + '" data-score="' + (server.redTeamScore + ':' + server.blueTeamScore) + '" data-winner="' + server.winner + '"' + (server.myTeam ? ' data-myteam="' + server.myTeam + '"' : '') + (server.myTeamWon ? ' data-myteamwon="' + server.myTeamWon + '"' : '') + (server.myTeamName ? ' data-myteamname="' + server.myTeamName + '"' : '') + ' />'; | |
svg += '<line x1="' + x1 + '" y1="' + (server.winner === 1 || server.winner === -1 ? y - 3 : y) + '" x2="' + x1 + '" y2="' + (server.winner === 2 || server.winner === -1 ? y + 3 : y) + '" class="SRC_GameStats_Graph_Details ' + (server.winner === 1 ? 'SRC_GameStats_Graph_GameStart_Red' : server.winner === 2 ? 'SRC_GameStats_Graph_GameStart_Blue' : 'SRC_GameStats_Graph_GameStart_Abandoned') + '" data-gameid="' + server.gameId + '" data-map="' + server.map + '" data-name="' + server.server + '" data-date="' + dayjs(server.started).format('DD-MMM-YYYY h:mma') + '" data-duration="' + tagpro.helpers.timeFromSeconds(server.duration, true) + '" data-score="' + (server.redTeamScore + ':' + server.blueTeamScore) + '" data-winner="' + server.winner + '"' + (server.myTeam ? ' data-myteam="' + server.myTeam + '"' : '') + (server.myTeamWon ? ' data-myteamwon="' + server.myTeamWon + '"' : '') + (server.myTeamName ? ' data-myteamname="' + server.myTeamName + '"' : '') + ' />'; | |
if (server.myTeam) { | |
if (server.myTeamWon) { | |
svg += '<circle cx="' + x1 + '" cy="' + (y - 6) + '" r="2" fill="#0e2" opacity="0.5" shape-rendering="auto" title="My Game" />' | |
} else { | |
svg += '<circle cx="' + x1 + '" cy="' + (y - 6) + '" r="2" fill="#aaa" opacity="0.5" shape-rendering="auto" title="My Game" />' | |
} | |
} | |
} | |
svg += '<text x="' + (graphPaddingLeft - 30) + '" y="' + (graphHeight - 3) + '" class="SRC_GameStats_Graph_LabelX" text-anchor="start">' + dayjs(firstGameStarted).format('h:mma') + '</text>'; | |
svg += '<line x1="' + (graphPaddingLeft - 1) + '" y1="0" x2="' + (graphPaddingLeft - 1) + '" y2="' + (graphHeight - 10) + '" class="SRC_GameStats_Graph_Guide" />'; | |
const firstTime = new Date(firstGameStarted); | |
const firstHour = new Date(firstGameStarted).getHours() + 1; | |
const lastHour = new Date().getHours(); | |
for (let x = 1; x <= lastHour - firstHour + 1; x++) { | |
firstTime.setHours(firstHour + x - 1, 0, 0, 0); | |
let x1 = graphPaddingLeft + ((firstTime.getTime() - firstGameStarted) / timePeriodMs) * graphWidth; | |
let hour = firstHour + x - 1; | |
svg += '<text x="' + (x === 1 ? x1 + 22 : x === lastHour - firstHour + 1 ? x1 - 22 : x1) + '" y="' + (graphHeight - 3) + '" class="SRC_GameStats_Graph_LabelX" text-anchor="middle">' + (hour > 12 ? hour - 12 + ':00pm' : hour + ':00am') + '</text>'; | |
svg += '<line x1="' + x1 + '" y1="0" x2="' + x1 + '" y2="' + (graphHeight - 10) + '" class="SRC_GameStats_Graph_Guide" />'; | |
} | |
svg += '<text x="' + (graphPaddingLeft + graphWidth - 3)+ '" y="' + (graphHeight - 3) + '" class="SRC_GameStats_Graph_LabelX" text-anchor="end">' + dayjs(now).format('hh:mma') + '</text>'; | |
svg += '<line x1="' + (graphPaddingLeft + graphWidth) + '" y1="0" x2="' + (graphPaddingLeft + graphWidth) + '" y2="' + (graphHeight - 10) + '" class="SRC_GameStats_Graph_Guide" />'; | |
$('#SRC_GameStats_Graph_SVG').remove(); | |
$('#SRC_GameStats_Graph_Container').append(svg + '</svg>'); | |
}; | |
let createScoreboardData = function(players) { | |
let data = []; | |
let totals = { | |
tags: 0, | |
pops: 0, | |
grabs: 0, | |
drops: 0, | |
hold: 0, | |
captures: 0, | |
prevent: 0, | |
returns: 0, | |
support: 0, | |
powerups: 0, | |
}; | |
for (let playerId in players) { | |
for (let stat in totals) { | |
totals[stat] += players[playerId]['s-' + stat]; | |
} | |
} | |
for (let playerId in players) { | |
if (players[playerId].finished) { | |
let thisData = { | |
id: playerId, | |
name: players[playerId].name, | |
team: players[playerId].team, | |
auth: players[playerId].auth, | |
userId: players[playerId].userId, | |
tags: players[playerId]['s-tags'], | |
pops: players[playerId]['s-pops'], | |
grabs: players[playerId]['s-grabs'], | |
drops: players[playerId]['s-drops'], | |
hold: players[playerId]['s-hold'], | |
captures: players[playerId]['s-captures'], | |
prevent: players[playerId]['s-prevent'], | |
returns: players[playerId]['s-returns'], | |
support: players[playerId]['s-support'], | |
powerups: players[playerId]['s-powerups'], | |
oldscore: 0, | |
oldpos: '', | |
score: +players[playerId].score, | |
oscore: +players[playerId].oscore, | |
dscore: +players[playerId].dscore, | |
points: players[playerId].points, | |
otspawn: players[playerId].otspawn || 0, | |
joined: players[playerId].joined, | |
}; | |
for (let stat in totals) { | |
thisData.oldscore += players[playerId]['s-' + stat] / (totals[stat] || 1) * (stat === 'pops' || stat === 'drops' ? -50 : 50); | |
} | |
data.push(thisData); | |
} | |
} | |
data.sort(function(a, b) { | |
return (b.oldscore - a.oldscore ? b.oldscore - a.oldscore : a.id - b.id); | |
}); | |
for (let i = 0; i < data.length; i++) { | |
data[i].oldpos = i + 1; | |
} | |
data.sort(function(a, b) { | |
return (b.score - a.score ? b.score - a.score : a.id - b.id); | |
}); | |
return data; | |
}; | |
let appendScoreboard = function(gameId, targetElement = '#SRC_ChatLog_Chat') { | |
let playersSorted = createScoreboardData(cachedGames[gameId].players); | |
let actualGameStarted = cachedGames[gameId].metadata.started; | |
let actualGameDuration = cachedGames[gameId].metadata.duration; | |
let gameFinish = cachedGames[gameId].metadata.started + cachedGames[gameId].metadata.duration; | |
let isOvertime = cachedGames[gameId].metadata.overtimeStartedAt; | |
let stats = [ 'oldscore', 'score', 'oscore', 'dscore', 'tags', 'pops', 'grabs', 'drops', 'hold', 'captures', 'prevent', 'returns', 'support', 'powerups', 'points', 'otspawn' ]; | |
let rStats = { oldscore:0, score:0, oscore:0, dscore:0, tags:0, pops:0, grabs:0, drops:0, hold:0, captures:0, prevent:0, returns:0, support:0, powerups:0, points:0, otspawn:0 }; | |
let bStats = { oldscore:0, score:0, oscore:0, dscore:0, tags:0, pops:0, grabs:0, drops:0, hold:0, captures:0, prevent:0, returns:0, support:0, powerups:0, points:0, otspawn:0 }; | |
let lastColumn = isOvertime ? stats.length + 1 : stats.length; | |
let scoreboard = '<div style="margin:20px 80px 10px; border-bottom:1px solid #555;"></div>' + | |
'<table id="SRC_Scoreboard"><tr id="SRC_Scoreboard_Header"><td style="width:72px;"></td><td>Old</td><td>Score</td><td>O</td><td>D</td><td>Tags</td><td>Pops</td><td>Grabs</td><td>Drops</td><td>Hold</td><td>Caps</td><td>Prev</td><td>Rets</td><td>Supp</td><td>Pups</td><td>Pts</td>' + (isOvertime ? '<td>O/T</td>' : '') + '<td>Time</td></tr>'; | |
$.each(playersSorted, function(key, value) { | |
let timePlayed = Math.floor(gameFinish - Math.max(value.joined, actualGameStarted)); | |
scoreboard += '<tr data-team="' + value.team + '"><td style="color:' + (value.team === 1 ? redTextColor : blueTextColor) + '">' + (value.auth ? '<span class="SRC_Auth">✔</span>' : '') + (value.userId ? '<a href="/profile/' + value.userId + '" class="SRC_Scoreboard_UserId" target="_blank" title="View TagPro Profile (New Tab)">' + value.name + '</a>' : value.name) + '</td><td data-raw="'+value.oldpos+'" title="'+value.oldscore.toFixed(0)+'">'+value.oldpos.toFixed(0)+'</td><td data-raw="'+value.score+'">'+value.score+'</td><td data-raw="'+value.oscore+'">'+value.oscore+'</td><td data-raw="'+value.dscore+'">'+value.dscore+'</td><td data-raw="'+value.tags+'">'+value.tags+'</td><td data-raw="'+value.pops+'">'+value.pops+'</td><td data-raw="'+value.grabs+'">'+value.grabs+'</td><td data-raw="'+value.drops+'">'+value.drops+'</td><td data-raw="'+value.hold+'">'+tagpro.helpers.timeFromSeconds(value.hold, true)+'</td><td data-raw="'+value.captures+'">'+value.captures+'</td><td data-raw="'+value.prevent+'">'+tagpro.helpers.timeFromSeconds(value.prevent, true)+'</td><td data-raw="'+value.returns+'">'+value.returns+'</td><td data-raw="'+value.support+'">'+value.support+'</td><td data-raw="'+value.powerups+'">'+value.powerups+'</td><td data-raw="'+value.points+'">'+(value.points ? value.points : '-')+'</td>' + (isOvertime ? '<td data-raw="'+value.otspawn+'">'+(value.otspawn ? value.otspawn : '-')+'</td>' : '') + '<td title="Played for ' + tagpro.helpers.timeFromSeconds(Math.floor(timePlayed / 1000), true) + '">' + (timePlayed / actualGameDuration * 100).toFixed(0) + '%</td></tr>'; | |
for (let v of stats) { | |
if (value.team === 1) { | |
rStats[v] += value[v]; | |
} else if (value.team === 2) { | |
bStats[v] += value[v]; | |
} | |
} | |
}); | |
scoreboard += '</table>'; | |
scoreboard += '<div style="margin:10px 80px 30px; border-bottom:1px solid #555;"></div>'; | |
$(targetElement).append(scoreboard); | |
setTimeout(() => { | |
for (let i = 2; i <= lastColumn; i++) { | |
let column = $('#SRC_Scoreboard tr:gt(0) td:nth-child(' + i + ')'); | |
let prevMax = 0; | |
$(column).each(function(k, v) { | |
if ($(this).data('raw') > prevMax) { | |
$(column).removeClass('SRC_SB_Max_Red SRC_SB_Max_Blue'); | |
$(this).addClass($(this).parent().data('team') === 1 ? 'SRC_SB_Max_Red' : 'SRC_SB_Max_Blue'); | |
prevMax = $(this).data('raw'); | |
} else if ($(this).data('raw') === prevMax && prevMax !== 0) { | |
$(this).addClass($(this).parent().data('team') === 1 ? 'SRC_SB_Max_Red' : 'SRC_SB_Max_Blue'); | |
} | |
}); | |
} | |
$('#SRC_Scoreboard').append('<tr><td colspan="100" style="background:#696969;"></td></tr>'); | |
$('#SRC_Scoreboard').append('<tr><td style="color:' + redTextColor + '; text-align:right">Red:</td><td'+(rStats.oldscore>=bStats.oldscore?' class="SRC_SB_Max_Red"':'')+'>'+rStats.oldscore.toFixed(0)+'</td><td'+(rStats.score>=bStats.score?' class="SRC_SB_Max_Red"':'')+'>'+rStats.score+'</td><td'+(rStats.oscore>=bStats.oscore?' class="SRC_SB_Max_Red"':'')+'>'+rStats.oscore+'</td><td'+(rStats.dscore>=bStats.dscore?' class="SRC_SB_Max_Red"':'')+'>'+rStats.dscore+'</td><td'+(rStats.tags>=bStats.tags?' class="SRC_SB_Max_Red"':'')+'>'+rStats.tags+'</td><td'+(rStats.pops>=bStats.pops?' class="SRC_SB_Max_Red"':'')+'>'+rStats.pops+'</td><td'+(rStats.grabs>=bStats.grabs?' class="SRC_SB_Max_Red"':'')+'>'+rStats.grabs+'</td><td'+(rStats.drops>=bStats.drops?' class="SRC_SB_Max_Red"':'')+'>'+rStats.drops+'</td><td'+(rStats.hold>=bStats.hold?' class="SRC_SB_Max_Red"':'')+'>'+tagpro.helpers.timeFromSeconds(rStats.hold, true)+'</td><td'+(rStats.captures>=bStats.captures?' class="SRC_SB_Max_Red"':'')+'>'+rStats.captures+'</td><td'+(rStats.prevent>=bStats.prevent?' class="SRC_SB_Max_Red"':'')+'>'+tagpro.helpers.timeFromSeconds(rStats.prevent, true)+'</td><td'+(rStats.returns>=bStats.returns?' class="SRC_SB_Max_Red"':'')+'>'+rStats.returns+'</td><td'+(rStats.support>=bStats.support?' class="SRC_SB_Max_Red"':'')+'>'+rStats.support+'</td><td'+(rStats.powerups>=bStats.powerups?' class="SRC_SB_Max_Red"':'')+'>'+rStats.powerups+'</td><td'+(rStats.points>=bStats.points?' class="SRC_SB_Max_Red"':'')+'>'+(rStats.points ? rStats.points : '-')+'</td>' + (isOvertime ? '<td'+(rStats.otspawn>=bStats.otspawn?' class="SRC_SB_Max_Red"':'')+'>'+(rStats.otspawn ? rStats.otspawn : '-')+'</td>' : '') + '<td>-</td></tr>'); | |
$('#SRC_Scoreboard').append('<tr><td style="color:' + blueTextColor + '; text-align:right">Blue:</td><td'+(bStats.oldscore>=rStats.oldscore?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.oldscore.toFixed(0)+'</td><td'+(bStats.score>=rStats.score?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.score+'</td><td'+(bStats.oscore>=rStats.oscore?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.oscore+'</td><td'+(bStats.dscore>=rStats.dscore?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.dscore+'</td><td'+(bStats.tags>=rStats.tags?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.tags+'</td><td'+(bStats.pops>=rStats.pops?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.pops+'</td><td'+(bStats.grabs>=rStats.grabs?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.grabs+'</td><td'+(bStats.drops>=rStats.drops?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.drops+'</td><td'+(bStats.hold>=rStats.hold?' class="SRC_SB_Max_Blue"':'')+'>'+tagpro.helpers.timeFromSeconds(bStats.hold, true)+'</td><td'+(bStats.captures>=rStats.captures?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.captures+'</td><td'+(bStats.prevent>=rStats.prevent?' class="SRC_SB_Max_Blue"':'')+'>'+tagpro.helpers.timeFromSeconds(bStats.prevent, true)+'</td><td'+(bStats.returns>=rStats.returns?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.returns+'</td><td'+(bStats.support>=rStats.support?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.support+'</td><td'+(bStats.powerups>=rStats.powerups?' class="SRC_SB_Max_Blue"':'')+'>'+bStats.powerups+'</td><td'+(bStats.points>=rStats.points?' class="SRC_SB_Max_Blue"':'')+'>'+(bStats.points ? bStats.points : '-')+'</td>' + (isOvertime ? '<td'+(bStats.otspawn>=rStats.otspawn?' class="SRC_SB_Max_Blue"':'')+'>'+(bStats.otspawn ? bStats.otspawn : '-')+'</td>' : '') + '<td>-</td></tr>'); | |
}, 100); | |
}; | |
let drawLine = function(ctx, x1, y1, x2, y2, color='#ffffff', lineWidth=1, dash=[]) { | |
ctx.setLineDash(dash); | |
ctx.beginPath(); | |
ctx.lineWidth = lineWidth; | |
ctx.strokeStyle = color; | |
ctx.moveTo(x1, y1); | |
ctx.lineTo(x2, y2); | |
ctx.stroke(); | |
ctx.closePath(); | |
}; | |
let appendCapsTimeline = function(gameId, totalWidth = 380) { | |
const metadata = cachedGames[gameId].metadata; | |
const totalHeight = 50; | |
const centerY = totalHeight / 2; | |
const minuteMarkerLargeSize = 7; | |
const offsetYMarkerLargeRed = centerY - minuteMarkerLargeSize; | |
const offsetYMarkerLargeBlue = centerY + minuteMarkerLargeSize; | |
const minuteMarkerSmallSize = 4; | |
const offsetYMarkerSmallRed = centerY - minuteMarkerSmallSize; | |
const offsetYMarkerSmallBlue = centerY + minuteMarkerSmallSize; | |
// S3 = Pre-Game; S1 = Normal Time; S5 = Overtime | |
const expectedS1Duration = metadata.expectedGameDuration; // 360000 = 6 mins for pubs | |
const actualS3Duration = metadata.preGameDuration; | |
const actualS5Duration = metadata.overtimeStartedAt ? metadata.started + actualS3Duration + metadata.duration - metadata.overtimeStartedAt : 0; | |
const actualS1Duration = metadata.duration - actualS5Duration; | |
const expectedS3S1Duration = actualS3Duration + expectedS1Duration; | |
const actualS3S1Duration = actualS3Duration + actualS1Duration; | |
const actualS1S5Duration = expectedS1Duration + actualS5Duration; | |
const actualS3S1S5Duration = actualS3Duration + expectedS1Duration + actualS5Duration; | |
const s1GameLengthMins = Math.ceil((expectedS1Duration) / 1000 / 60); | |
const s1S5GameLengthMins = Math.ceil(actualS1S5Duration / 1000 / 60); | |
const s1StartPoint = actualS3Duration / actualS3S1S5Duration; | |
const s1EndPointExpected = expectedS3S1Duration / actualS3S1S5Duration; | |
const s1EndPointActual = actualS3S1Duration / actualS3S1S5Duration; | |
const minutesInterval = (totalWidth - (s1StartPoint * totalWidth)) / (actualS1S5Duration / 60000); | |
$('#SRC_CapTimeline_Container').empty(); | |
$('#SRC_CapTimeline_Container').append('<canvas id="SRC_CapTimelineCanvas" width="' + (totalWidth + 20) + '" height="' + totalHeight + '"></div>'); | |
let canvas = $('#SRC_CapTimelineCanvas'); | |
let ctx = canvas.get(0).getContext('2d'); | |
ctx.translate(10.5, 0.5); | |
// minute markers... | |
let count = 0; | |
for (let x = 0; x <= totalWidth; x += minutesInterval) { | |
if (x === 0) { // start marker | |
drawLine(ctx, s1StartPoint * totalWidth, offsetYMarkerLargeRed, s1StartPoint * totalWidth, offsetYMarkerLargeBlue, 'white'); | |
} else if (s1GameLengthMins % 2 === 0 && count === s1GameLengthMins / 2) { // halfway marker in normal time (only if even minutes) | |
drawLine(ctx, s1StartPoint * totalWidth + x, offsetYMarkerLargeRed, s1StartPoint * totalWidth + x, offsetYMarkerLargeBlue, 'white'); | |
} else if (count === s1GameLengthMins || count === s1S5GameLengthMins) { // end of normal time marker | |
drawLine(ctx, s1StartPoint * totalWidth + x, offsetYMarkerLargeRed, s1StartPoint * totalWidth + x, offsetYMarkerLargeBlue, 'white'); | |
} else { // minute marker | |
drawLine(ctx, s1StartPoint * totalWidth + x, offsetYMarkerSmallRed, s1StartPoint * totalWidth + x, offsetYMarkerSmallBlue, 'white'); | |
} | |
count++; | |
} | |
// horizontal game-section lines... | |
drawLine(ctx, 0, centerY, s1StartPoint * totalWidth, centerY, 'red', 1, [2, 2]); // pre-game | |
if (actualS5Duration) { // overtime | |
drawLine(ctx, s1StartPoint * totalWidth, centerY, s1EndPointExpected * totalWidth, centerY, '#eee'); // normal game time played | |
drawLine(ctx, s1EndPointExpected * totalWidth, centerY, totalWidth, centerY, 'orange'); // overtime played | |
drawLine(ctx, totalWidth, offsetYMarkerLargeRed, totalWidth, offsetYMarkerLargeBlue, 'orange'); // end of overtime vertical marker | |
} else { | |
drawLine(ctx, s1StartPoint * totalWidth, centerY, s1EndPointActual * totalWidth, centerY, '#eee'); // normal game time played | |
drawLine(ctx, s1EndPointActual * totalWidth, centerY, s1EndPointExpected * totalWidth, centerY, '#666', 1, [2, 2]); // post-game time (not played) | |
} | |
// caps... | |
ctx.globalAlpha = 0.85; | |
for (let chat of cachedGames[gameId].chatlog) { | |
if (chat.cap) { | |
const x = Math.min((chat.time / actualS3S1S5Duration * totalWidth), totalWidth); | |
ctx.fillStyle = chat.cap === 1 ? '#ff4747' : '#4494f2'; | |
ctx.beginPath(); | |
ctx.arc(x, centerY, 3.5, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
} | |
// joiners/leavers/switchers... | |
ctx.globalAlpha = 0.85; | |
for (const player of cachedGames[gameId].joiners) { | |
const color = player.team === 1 ? '#ff4747' : '#4494f2'; | |
const x = Math.round(Math.min((player.time / actualS3S1S5Duration * totalWidth), totalWidth)); | |
if (player.type === 'left') { //add '-' where player left... | |
const y = player.team === 1 ? centerY - 12 : centerY + 12; | |
drawLine(ctx, x - 1.5, y, x + 1.5, y, color); | |
} else if (player.type === 'joined') { //add '+' where player joined... | |
let y = player.team === 1 ? centerY - 9 : centerY + 9; | |
drawLine(ctx, x, y - 1.5, x, y + 1.5, color); | |
drawLine(ctx, x - 1.5, y, x + 1.5, y, color); | |
} else if (player.type === 'switched') { //mark where player switched teams... | |
const fromTeamColor = player.newTeam === 1 ? '#4494f2' : '#ff4747'; | |
const toTeamColor = player.newTeam === 1 ? '#ff4747' : '#4494f2'; | |
let y = player.newTeam === 1 ? centerY - 16 : centerY + 16; | |
drawLine(ctx, x + 0, y - 1.5, x + 0, y + 1.5, toTeamColor); | |
drawLine(ctx, x + 1, y - 1.5, x + 1, y + 1.5, toTeamColor); | |
} | |
} | |
ctx.translate(-10.5, -0.5); | |
}; | |
let saveChatEntry = function(chatlog, data, time, players, currentState, selectedFlair) { | |
let name = null; | |
let team = null; | |
if (data.from > 0) { | |
name = players['' + data.from].name; | |
team = players['' + data.from].team; | |
} else { | |
name = data.from; | |
} | |
chatlog.push({ from:data.from, message:data.message, to:data.to, c:data.c, mod:data.mod, monitor:data.monitor, time:time, name:name, team:team, state:currentState, flair:selectedFlair }); | |
}; | |
let showChatLog = function(gameId, addReplayLink = false) { | |
let chatlog = cachedGames[gameId].chatlog; | |
let metadata = cachedGames[gameId].metadata; | |
let gameStartedAt = new Date(metadata.started); | |
let overtimeStartedAt = metadata.overtimeStartedAt; | |
if (!$('#SRC_ChatLog').length) { | |
addMainContainer(); | |
} | |
$('#SRC_CapTimeline_Container').empty(); | |
$('#SRC_ChatLog_Chat').empty(); | |
$('#SRC_ChatLog_Chat').append('<div class="SRC_Header">' + gameStartedAt.toDateString() + ', ' + gameStartedAt.toLocaleTimeString() + '</div>'); | |
$('#SRC_ChatLog_Chat').append('<div class="SRC_Header">' + metadata.mapName + '</div>'); | |
$('#SRC_ChatLog_Chat').append('<div class="SRC_Header">Game ID: ' + metadata.gameId + ' on ' + metadata.serverName + '</div>'); | |
$('#SRC_ChatLog_Chat').append('<div class="SRC_Header">Final Score: ' + metadata.teams.red.score + ':' + metadata.teams.blue.score + '</div>'); | |
if (addReplayLink) { | |
let replayUrl = getReplayUrl(gameId, userId); | |
$('#SRC_ChatLog_Chat').append('<div class="SRC_Header"><a href="' + replayUrl + '" style="color:#689F38; text-decoration:none;" target="_blank">View Replay</a></div>'); | |
} | |
$('#SRC_ChatLog_Chat').append('<div style="margin:3px 80px; border-bottom:1px solid #555;"></div>'); | |
for (let i = 0; i < chatlog.length; i++) { | |
let fromColor = ''; | |
let messageColor = ''; | |
let timerText = ''; | |
let flair; | |
if (chatlog[i].to === 'team') { | |
fromColor = chatlog[i].team === 1 ? redTextColor : blueTextColor; | |
messageColor = fromColor; | |
} else if (chatlog[i].to === 'group') { | |
fromColor = chatlog[i].team === 1 ? redTextColor : blueTextColor; | |
messageColor = '#e7e700'; | |
} else if (chatlog[i].to === 'mods') { | |
fromColor = chatlog[i].team === 1 ? redTextColor : blueTextColor; | |
messageColor = '#00b900'; | |
} else if (chatlog[i].mod) { | |
fromColor = '#00b900'; | |
messageColor = fromColor; | |
} else if (chatlog[i].monitor) { | |
fromColor = '#00b7a5'; | |
messageColor = fromColor; | |
} | |
if (chatlog[i].c) { | |
fromColor = chatlog[i].c; | |
messageColor = fromColor; | |
} | |
if (chatlog[i].state === 2 || chatlog[i].state === 3) { | |
timerText = '--:--'; | |
} else if (metadata.gameEndsAt) { | |
let timeLeft = Math.round(((metadata.gameEndsAt - metadata.gameStartedAt + metadata.preGameDuration) - chatlog[i].time) / 1000); | |
if (timeLeft < 0) { | |
timerText = '+' + tagpro.helpers.timeFromSeconds(Math.abs(timeLeft), true); | |
} else { | |
timerText = tagpro.helpers.timeFromSeconds(timeLeft, true); | |
} | |
} | |
if (chatlog[i].flair) { | |
flair = '<span class="flair ' + chatlog[i].flair.className + '" style="background-position: -' + (chatlog[i].flair.x * 16) + 'px -' + (chatlog[i].flair.y * 16) + 'px; scale:70%; margin:-1px 1px -4px 1px;"></span>'; | |
} | |
$('#SRC_ChatLog_Chat').append('<div class="SRC_Chat"' + (chatlog[i].cap ? ' data-score="' + chatlog[i].scoreRed + ':' + chatlog[i].scoreBlue + '"' : '') + '>' + | |
' <span class="SRC_Timer' + (chatlog[i].cap ? (chatlog[i].cap === 1 ? ' SRC_Cap_Red' : ' SRC_Cap_Blue') : '') + '"' + ' data-time="' + chatlog[i].time + '">' + timerText + '</span>' + | |
' <span class="SRC_Message"' + (messageColor ? ' style="color:' + messageColor + ';"' : '') + '>' + | |
(chatlog[i].name ? '<span class="' + (chatlog[i].team === 1 ? 'SRC_TeamChatRed' : 'SRC_TeamChatBlue') + '"' + (fromColor ? ' style="color:' + fromColor + ';"' : '') + '>' + chatlog[i].name + '</span><span style="color:#ccc;">:</span> ' : '') + | |
' <span class="SRC_ActualMessage"' + (messageColor ? ' style="color:' + messageColor + ';"' : '') + '>' + chatlog[i].message + '</span>' + (flair ? flair : '') + | |
' </span>' + | |
'</div>'); | |
} | |
appendScoreboard(gameId); | |
appendCapsTimeline(gameId); | |
$('#SRC_ChatLog').fadeIn(200); | |
}; | |
let getReplay = function(gameId) { | |
let url = '/replays/gameFile?gameId=' + gameId + (userId ? '&userId=' + userId : ''); | |
$('#SRC_CapTimeline_Container').empty(); | |
$('#SRC_ChatLog_Chat').empty(); | |
$('#SRC_ChatLog_Chat').append('<div style="margin:50px auto; text-align:center; font-size:14px; color:#0b4;">Getting data...</div>'); | |
$('#SRC_ChatLog').fadeIn(200); | |
return $.get(url).done(function(data) { | |
if (data.error) { | |
$('#SRC_ChatLog_Chat').append('<div style="margin:10px auto; text-align:center; font-size:12px; color:#c00;">A server error has occured: ' + data.error + '</div>'); | |
console.log('SRC:: A server error has occured (0):\n' + data.error); | |
return false; | |
} else { | |
lastGet = Date.now(); | |
return data; | |
} | |
}).fail(function(jqxhr, textStatus, error) { | |
$('#SRC_ChatLog_Chat').append('<div style="margin:10px auto; text-align:center; font-size:12px; color:#c00;">A server error has occured: ' + error + '</div>'); | |
console.log('SRC:: A server error has occured (1):\nStatus:', jqxhr.status, '\nError:', error); | |
return jqxhr; | |
}); | |
}; | |
let processReplay = function(data, gameId) { | |
let chatlog = []; | |
let players = {}; | |
let currentPlayers = {}; // this is to track each player's name and team during the game | |
let joiners = []; // and leavers and switchers | |
let metadata = null; | |
let currentId = null; | |
let currentState = null; | |
let currentRedCount = 0; | |
let currentBlueCount = 0; | |
let currentScoreRed, currentScoreBlue; | |
let stats = ['auth','points','score','oscore','dscore','s-tags','s-pops','s-grabs','s-drops','s-hold','s-captures','s-prevent','s-returns','s-support','s-powerups']; | |
let packets; | |
let skipCapturesMaps = ['Easter Race']; // names of maps to not process the 's-captures' message for (event maps where the captures stat is used for something else) | |
let skipCaptures = false; | |
let skipNextChatMessage = ''; | |
if (Array.isArray(data)) { | |
packets = data; | |
} else { | |
packets = data.split('\n'); | |
} | |
// console.log('SRC:: processReplay() packets:', packets); | |
for (let i = 0; i < packets.length; i++) { | |
const packet = Array.isArray(packets[i]) ? packets[i] : JSON.parse(packets[i]); | |
if (packet[1] === 'p') { | |
const arr = packet[2]; | |
for (let idx in arr) { | |
const obj = arr[idx]; | |
for (let key in obj) { | |
if (stats.indexOf(key) >= 0) { | |
if (!skipCaptures && key === 's-captures' && obj[key] && players[obj.id][key] !== obj[key] && currentPlayers[obj.id]) { | |
chatlog.push({ from:null, message:(currentPlayers[obj.id].team === 1 ? metadata.teams.red.name : metadata.teams.blue.name) + ' Caps! ' + currentPlayers[obj.id].name + ' (' + currentScoreRed + ':' + currentScoreBlue + '), ' + currentRedCount + 'v' + currentBlueCount, to:'all', c:'#7fff00', mod:null, monitor:null, time:packet[0], state:currentState, cap:currentPlayers[obj.id].team, scoreRed:currentScoreRed, scoreBlue:currentScoreBlue }); | |
} | |
players[obj.id][key] = obj[key]; | |
} | |
if (key === 'flair') { | |
// show ALL new flair messages (they're sometimes hidden if not "for" us)... | |
if (currentState === 2 && players[obj.id].flair && players[obj.id].flair.key !== obj.flair.key) { // currentPlayers[obj.id].name && obj.id === lastFlairReceiverId | |
chatlog.push({ from:null, message:obj.name + ' has ' + skipNextChatMessage + ' ' + obj.flair.description + ' flair!' + '<span class="flair ' + obj.flair.className + '" style="background-position: -' + (obj.flair.x * 16) + 'px -' + (obj.flair.y * 16) + 'px; scale:75%; margin:-1px 1px -4px 1px;"></span>' + obj.flair.extra, to:'all', c:'#BFFF00', mod:null, monitor:null, time:packet[0], state:currentState }); | |
} | |
players[obj.id].flair = obj.flair; | |
skipNextChatMessage = ''; | |
} | |
} | |
} | |
} else if (packet[1] === 'mark') { | |
chatlog.push({ from:null, message:'<span style="display:flex; align-items:center; background:#000; padding:0 10px; font-weight:bold; border-radius:5px;">- MARK -</span>', to:'all', c:'#b0b', mod:null, monitor:null, time:packet[0], state:currentState }); | |
} else if (packet[1] === 'chat') { | |
if (packet[2].from !== null) { | |
saveChatEntry(chatlog, packet[2], packet[0], players, currentState); | |
} else if (packet[2].from === null && (packet[2].to === 'all' || packet[2].to == packet[2].for) && packet[2].c !== '#63FE22') { // never show tips | |
if (packet[2].to === 'all') { | |
let selectedFlair; | |
let message = String(packet[2].message); | |
if (currentState !== 2) { | |
const hasJoined = message.lastIndexOf(' has joined'); | |
const hasLeft = message.lastIndexOf(' has left'); | |
const hasSwitched = message.lastIndexOf(' has switched'); | |
if (hasJoined >= 0) { | |
const name = message.slice(0, hasJoined); | |
if (message.includes('the ' + metadata.teams.red.name + ' team')) { | |
currentRedCount++; | |
currentPlayers[packet[2].for] = { name:name, team:1 }; | |
joiners.push({ time:packet[0], id:packet[2].for, team:1, type:'joined' }); | |
} else if (message.includes('the ' + metadata.teams.blue.name + ' team')) { | |
currentBlueCount++; | |
currentPlayers[packet[2].for] = { name:name, team:2 }; | |
joiners.push({ time:packet[0], id:packet[2].for, team:2, type:'joined' }); | |
} | |
selectedFlair = players[packet[2].for] && players[packet[2].for].flair ? players[packet[2].for].flair : false; | |
} else if (hasLeft >= 0) { | |
const name = message.slice(0, hasLeft); | |
if (message.includes('the ' + metadata.teams.red.name + ' team')) { | |
currentRedCount--; | |
currentPlayers[packet[2].for] = { name:name, team:1 }; | |
joiners.push({ time:packet[0], id:packet[2].for, team:1, type:'left' }); | |
} else if (message.includes('the ' + metadata.teams.blue.name + ' team')) { | |
currentBlueCount--; | |
currentPlayers[packet[2].for] = { name:name, team:2 }; | |
joiners.push({ time:packet[0], id:packet[2].for, team:2, type:'left' }); | |
} | |
} else if (hasSwitched >= 0) { | |
const name = message.slice(0, hasSwitched); | |
if (message.includes('the ' + metadata.teams.red.name + ' team')) { | |
currentRedCount++; | |
currentBlueCount--; | |
currentPlayers[packet[2].for] = { name:name, team:1 }; | |
joiners.push({ time:packet[0], id:packet[2].for, newTeam:1, type:'switched' }); | |
} else if (message.includes('the ' + metadata.teams.blue.name + ' team')) { | |
currentBlueCount++; | |
currentRedCount--; | |
currentPlayers[packet[2].for] = { name:name, team:2 }; | |
joiners.push({ time:packet[0], id:packet[2].for, newTeam:2, type:'switched' }); | |
} | |
} | |
} | |
saveChatEntry(chatlog, packet[2], packet[0], players, currentState, selectedFlair); | |
} else if (packet[2].to == packet[2].for) { | |
let message = String(packet[2].message); | |
if (message.startsWith('During overtime, each death increases time to respawn.')) { | |
players[packet[2].for].otspawn = +message.slice(message.lastIndexOf(':') + 2); | |
} else if (message.startsWith('This is a save attempt!')) { | |
chatlog.push({ from:null, message:'<span>' + players[packet[2].for].name + ' - This is a save attempt! A loss will not negatively impact your win %.</span>', to:'all', c:'#fff', mod:null, monitor:null, time:packet[0], state:currentState }); | |
} | |
} | |
} | |
} else if (packet[1] === 'id') { | |
currentId = packet[2]; // not used by this script anymore ?? | |
} else if (packet[1] === 'score') { | |
currentScoreRed = packet[2].r; | |
currentScoreBlue = packet[2].b; | |
} else if (packet[1] === 'time' && currentState !== packet[2].state) { | |
currentState = packet[2].state; | |
if (currentState === 1) { | |
metadata.gameEndsAt = metadata.gameStartedAt + packet[2].time; | |
metadata.expectedGameDuration = packet[2].time; | |
chatlog.push({ from:null, message: '<span style="background:#ddd; padding:0 10px; font-weight:bold; border-radius:5px;">Start of Game</span>', to:'all', c:'#000', mod:null, monitor:null, time:packet[0], state:currentState }); | |
} else if (currentState === 3) { | |
metadata.preGameDuration = packet[2].time; | |
metadata.gameStartedAt = metadata.started + packet[2].time; | |
} else if (currentState === 5) { | |
metadata.overtimeStartedAt = metadata.gameEndsAt; | |
} | |
} else if (packet[1] === 'end') { | |
const winnerTeamText = packet[2].winner === 'red' ? 'Red ' : packet[2].winner === 'blue' ? 'Blue Wins ' : 'Tie '; | |
const winnerIconText = packet[2].winner === 'red' ? '<span style="margin:0 5px; font-size:8px;">🔴</span>' : packet[2].winner === 'blue' ? '<span style="margin:0 5px; font-size:8px;">🔵</span>' : '<span style="margin:0 5px; font-size:8px;">🟠</span>'; | |
currentState = 2; | |
chatlog.push({ from:null, message:'<span style="display:flex; align-items:center; background:#ddd; padding:0 10px; font-weight:bold; border-radius:5px;"><span>End of Game</span>' + winnerIconText + '<span>' + winnerTeamText + metadata.teams.red.score + ':' + metadata.teams.blue.score + '</span>' + winnerIconText, to:'all', c:'#000', mod:null, monitor:null, time:packet[0], state:currentState }); | |
} else if (packet[1] === 'recorder-metadata') { | |
for (let j = 0; j < packet[2].players.length; j++) { | |
players[packet[2].players[j].id] = { name:packet[2].players[j].displayName, team:packet[2].players[j].team, userId:packet[2].players[j].userId, joined:packet[2].players[j].joined, finished:packet[2].players[j].finished }; | |
} | |
metadata = packet[2]; | |
if (skipCapturesMaps.indexOf(metadata.mapName) >= 0) { | |
skipCaptures = true; | |
} | |
} | |
} | |
cachedGames[gameId] = { | |
dateCached: Date.now(), | |
chatlog: chatlog, | |
metadata: metadata, | |
players: players, | |
joiners: joiners, | |
}; | |
GM_setValue('cachedGames', cachedGames); | |
}; | |
let getProcessAndShowReplay = function(gameId, addReplayLink = false) { | |
getReplay(gameId).then(function(data) { | |
if (data.error) { | |
console.log('SRC:: getProcessAndShowReplay() data.error:', data); | |
} else { | |
processReplay(data, gameId); | |
showChatLog(gameId, addReplayLink); | |
$('a.SRC_ShowChat[data-gameid="' + gameId + '"]').find('i').addClass('SRC_Cached'); | |
} | |
}).fail(function(jqxhr) { | |
console.log('SRC:: getProcessAndShowReplay() .fail() jqxhr:', jqxhr); | |
if (jqxhr.status === 500) { | |
console.log('SRC:: getProcessAndShowReplay() .fail() 500 Internal Server Error'); | |
} | |
}); | |
}; | |
let clearChatSyncs = function() { | |
$('.SRC_LastChatSync').removeClass('SRC_LastChatSync'); | |
$('.SRC_SeenChatSync').removeClass('SRC_SeenChatSync'); | |
}; | |
let seekTimelineTo = function(seekTo) { | |
$('#replaySeekBar').val(seekTo); | |
$('#replaySeekBar').trigger('mouseup'); | |
}; | |
let syncLastChat = function(gameId) { | |
const actualMessages = $('#SRC_ChatLog_Chat').find('.SRC_ActualMessage'); | |
const teamNameRed = cachedGames[gameId].metadata.teams.red.name; | |
const teamNameBlue = cachedGames[gameId].metadata.teams.blue.name; | |
tagpro.socket.on('chat', function(data) { | |
$(actualMessages).each(function() { | |
if (!$(this).hasClass('SRC_SeenChatSync') && $(this).text() === data.message) { | |
$('.SRC_LastChatSync').removeClass('SRC_LastChatSync'); | |
$(this).addClass('SRC_SeenChatSync'); | |
$(this).parent().parent().addClass('SRC_LastChatSync'); | |
return false; | |
} | |
}); | |
}); | |
tagpro.socket.on('score', function(data) { | |
const message = data.r + ':' + data.b; | |
const target = $('#SRC_ChatLog_Chat').find('.SRC_Chat[data-score="' + message + '"]'); | |
if (target.length && !target.hasClass('SRC_SeenChatSync')) { | |
$('.SRC_LastChatSync').removeClass('SRC_LastChatSync'); | |
target.addClass('SRC_SeenChatSync SRC_LastChatSync'); | |
} | |
}); | |
$('#replaySeekBar, #replaySkipBack, #replaySkipForward').on('click', function() { | |
tagpro.renderer.options.disableAllExplosions = false; | |
clearChatSyncs(); | |
}); | |
$('#SRC_ChatLog_Chat').on('click', '.SRC_Timer', function() { | |
const seekTo = Math.max(1, this.dataset.time - 3000); | |
if (seekTo) { | |
clearChatSyncs(); | |
seekTimelineTo(seekTo); | |
$('.SRC_TimerClick').removeClass('SRC_TimerClick'); | |
$(this).addClass('SRC_TimerClick'); | |
} | |
}); | |
$('#SRC_ChatLog_Header').on('click', '#SRC_CapTimelineCanvas', function(e) { | |
if (e.offsetX < 10 || e.offsetX > 390) { | |
return; | |
} | |
const actualS3S1Duration = cachedGames[gameId].metadata.preGameDuration + cachedGames[gameId].metadata.expectedGameDuration; | |
const seekTo = Math.max(1, Math.floor(((e.offsetX - 10) / ($('#SRC_CapTimelineCanvas').width() - 20)) * Math.max(actualS3S1Duration, tagpro.replayData.maxTSSeek)) - 3000); | |
if (seekTo) { | |
clearChatSyncs(); | |
seekTimelineTo(seekTo); | |
} | |
}); | |
$('#SRC_ChatLog_Close').on('click', function() { | |
$('#SRC_ChatLog').remove(); | |
}); | |
}; | |
GM_addStyle('.table-teams tbody tr:hover { background:#244b20 !important; }'); | |
GM_addStyle('#SRC_ChatLog { display:none; position:absolute; padding:2px; color:#888; right:10px; margin:8px -40px 0 0; width:520px; background:#151515; border-radius:8px; box-shadow:8px 8px 10px black; font-size:11px; border:1px outset #88c; overflow:hidden; z-index:1; }'); | |
GM_addStyle('#SRC_ChatLog_Header { height:52px; }'); | |
GM_addStyle('#SRC_ChatLog_Chat { position:relative; height:420px; overflow-y:auto; overflow-x:hidden; }'); | |
GM_addStyle('#SRC_ChatLog_Chat::-webkit-scrollbar { width:4px; }'); | |
GM_addStyle('#SRC_ChatLog_Chat::-webkit-scrollbar-thumb { background:#292; border-radius:4px; }'); | |
GM_addStyle('#SRC_ChatLog_Chat::-webkit-scrollbar-track { background:#ddd; border-radius:4px; }'); | |
GM_addStyle('#SRC_ChatLog_Close { position:absolute; width:17px; height:17px; top:3px; right:2px; padding:0 0 0 1px; color:red; border:1px solid darkred; text-align:center; font-size:12px; font-weight:bold; border-radius:10px; cursor:pointer; }'); | |
GM_addStyle('#SRC_CapTimeline_Container { position:relative; text-align:center; height:50px; width:400px; margin:5px auto; background:rgba(0,0,0,0.3); border-radius:8px; }'); | |
GM_addStyle('.SRC_Cap_Red { background:' + redBackgroundColor + '; color:#fff !important; }'); | |
GM_addStyle('.SRC_Cap_Blue { background:' + blueBackgroundColor + '; color:#fff !important; }'); | |
GM_addStyle('.SRC_ShowChat { color:#ccc; margin:0 0 0 5px; }'); | |
GM_addStyle('.SRC_ShowChat i { color:#ccc; }'); | |
GM_addStyle('.SRC_Cached { color:#666 !important; }'); | |
GM_addStyle('.SRC_Header { text-align:center; }'); | |
GM_addStyle('.SRC_Timer { display:inline-block; color:#da981f; width:35px; min-width:35px; text-align:right; margin-right:3px; }'); | |
GM_addStyle('.SRC_Chat { display:flex; color:#ccc; }'); | |
GM_addStyle('.SRC_LastChatSync { background:#244b20; }'); | |
GM_addStyle('.SRC_TimerClick { text-decoration: underline; }'); | |
GM_addStyle('.SRC_TeamChatRed { color:' + redTextColor + '; }'); | |
GM_addStyle('.SRC_TeamChatBlue { color:' + blueTextColor + '; }'); | |
GM_addStyle('.SRC_Auth { font-size:9px; color:#77ff00; }'); | |
GM_addStyle('#SRC_Scoreboard { margin:5px auto; font-size:11px; color:#bbb; width:95%; text-align:center; background:#181818; }'); | |
GM_addStyle('#SRC_Scoreboard td { border-bottom:1px solid #000; padding:2px 1px 1px; white-space:nowrap; }'); | |
GM_addStyle('#SRC_Scoreboard td:nth-child(1) { font-family:monospace; }'); | |
GM_addStyle('#SRC_Scoreboard td:nth-child(3) { color:#888; }'); | |
GM_addStyle('#SRC_Scoreboard td:nth-child(4) { color:#888; }'); | |
GM_addStyle('#SRC_Scoreboard_Header { color:#fff; background:#696969; font-size:10px; }'); | |
GM_addStyle('#SRC_Scoreboard_Header td { color:#fff !important; }'); | |
GM_addStyle('.SRC_SB_Max_Red { color:white !important; background:' + redBackgroundColor + '; }'); | |
GM_addStyle('.SRC_SB_Max_Blue { color:white !important; background:' + blueBackgroundColor + '; }'); | |
GM_addStyle('.SRC_Scoreboard_UserId { color:inherit; text-decoration:none; }'); | |
GM_addStyle('.SRC_Scoreboard_UserId:hover, .SRC_Scoreboard_UserId:active, .SRC_Scoreboard_UserId:focus { color:inherit; text-decoration:1px dotted #999 underline; }'); | |
GM_addStyle('#SRC_GameStats_Show { position:absolute; right:18px; top:20px; color:#b655ff; font-size:12px; opacity:0.6; cursor:pointer; }'); | |
GM_addStyle('#SRC_GameStats_Show:hover { opacity:1; }'); | |
GM_addStyle('#SRC_GameStats_AddRows { position:absolute; right:40px; top:20px; color:#bf8d1d; font-size:12px; opacity:0.6; cursor:pointer; }'); | |
GM_addStyle('#SRC_GameStats_AddRows:hover { opacity:1; }'); | |
GM_addStyle('.SRC_Overtime { color:#d3a857; }'); | |
GM_addStyle('.SRC_RowNumber { color:#aaa; font-size:11px; margin-right:4px; }'); | |
tagpro.ready(function() { | |
if (document.location.href.includes('/game?replay=')) { | |
if (MINIMIZED_WIDTH > 0) { | |
const replayId = document.location.search.replace('?replay=', ''); // this is different to the gameId used to download the game file (so it will be different in the cache) | |
$('#exit').after('<div id="SRC_ChatLog" style="width:' + MINIMIZED_WIDTH + 'px; top:150px; right:45px;"><div id="SRC_ChatLog_Header"><div id="SRC_CapTimeline_Container"></div><span id="SRC_ChatLog_Close" title="Close">X</span></div><div id="SRC_ChatLog_Chat"></div></div>'); | |
$('#SRC_ChatLog').on('mouseenter', function() { | |
$('#SRC_ChatLog').animate({ width: 520 }, 150); | |
}).on('mouseleave', function() { | |
$('#SRC_ChatLog').animate({ width: MINIMIZED_WIDTH }, 150); | |
}); | |
if (!cachedGames[replayId]) { | |
processReplay(tagpro.replayData.packets, replayId); | |
} | |
showChatLog(replayId); | |
syncLastChat(replayId); | |
} | |
} else if (document.location.pathname === '/replays') { | |
waitForTRs(); | |
addMainContainer(); | |
// add sort to existing tables... | |
$('#replays-app').on('click', '.table-teams th', function() { | |
let thIndex = this.cellIndex; | |
let tdIndex = 0; | |
let sortasc = true; | |
let currentTab = $('ul.tab-list').find('li.active').attr('data-tab'); | |
let isCurrentTabAll = currentTab === 'all'; | |
if (thIndex === 0) tdIndex = thIndex; | |
else if (thIndex === 1 || thIndex === 2) tdIndex = thIndex + 1; | |
else if (thIndex === 3) tdIndex = isCurrentTabAll ? thIndex + 1 : thIndex; | |
else if (thIndex === 4 && !isCurrentTabAll) tdIndex = thIndex + 1; | |
else return; | |
if (!this.dataset.sortasc || this.dataset.sortasc === undefined || this.dataset.sortasc === 'false') { | |
this.dataset.sortasc = 'true'; | |
sortasc = true; | |
} else { | |
this.dataset.sortasc = 'false'; | |
sortasc = false; | |
} | |
$('.table-teams tbody tr').sort(function(a, b) { | |
if (thIndex === 1 || thIndex === 2 || thIndex === 3 && !isCurrentTabAll) { | |
if (sortasc) { | |
return a.children[tdIndex].innerText.localeCompare(b.children[tdIndex].innerText); | |
} else { | |
return b.children[tdIndex].innerText.localeCompare(a.children[tdIndex].innerText); | |
} | |
} else { | |
if (sortasc) { | |
return a.children[tdIndex].dataset.sortby - b.children[tdIndex].dataset.sortby; | |
} else { | |
return b.children[tdIndex].dataset.sortby - a.children[tdIndex].dataset.sortby; | |
} | |
} | |
}).appendTo( $('.table-teams tbody') ); | |
$('.table-teams thead th').css('text-decoration', 'none'); | |
$(this).css('text-decoration', 'underline'); | |
}); | |
$('#replays-app').css('font-size', '16px'); // font a bit smaller | |
$('#replays-app').on('click', 'li', function() { | |
if (this.dataset.tab === 'mine' || this.dataset.tab === 'favorite' || this.dataset.tab === 'all' || this.dataset.tab === 'search' || this.dataset.tab === 'upload' || this.dataset.tab === 'foruser') { | |
$('#replays-app').after( $('#SRC_ChatLog') ); | |
setTimeout(waitForTRs, 200); | |
$('#SRC_GameStats_AddRows').hide(); | |
if (this.dataset.tab === 'all') { | |
jsonData = []; | |
$('#SRC_GameStats_Show').show(); | |
if (!$('#filter').is(':visible')) { | |
$('#SRC_GameStats_AddRows').show(); | |
} | |
} | |
} | |
}); | |
$('#replays-app').on('click', '.fa-xmark, .fa-angles-left, .fa-angles-right', function() { | |
$('#replays-app').after( $('#SRC_ChatLog') ); | |
$('#SRC_GameStats_AddRows').show(); | |
setTimeout(waitForTRs, 200); | |
}); | |
$('#replays-app').on('click', '.btn-apply', function() { | |
$('#replays-app').after( $('#SRC_ChatLog') ); | |
$('#SRC_GameStats_AddRows').hide(); | |
setTimeout(waitForTRs, 200); | |
}); | |
$('#replays-app').on('click', 'span', function() { | |
if ($(this).text().match('Newer') || $(this).text().match('Older')) { // there's no id or class | |
$('#replays-app').after( $('#SRC_ChatLog') ); | |
setTimeout(waitForTRs, 200); | |
} | |
}); | |
$('#replays-app').on('click', '.filterToggle', function(e) { | |
e.stopPropagation(); | |
if ($('#filter').is(':visible')) { | |
$('#replays-app').after( $('#SRC_ChatLog') ); | |
setTimeout(waitForTRs, 200); | |
} | |
let currentTab = $('ul.tab-list').find('li.active').attr('data-tab'); | |
if (currentTab === 'all') { | |
$('#SRC_GameStats_AddRows').show(); | |
} else { | |
$('#SRC_GameStats_AddRows').hide(); | |
} | |
}); | |
$('#replays-app').on('click', '.SRC_ShowChat', function(e) { | |
e.preventDefault(); | |
let gameId = this.dataset.gameid; | |
//let replayId = this.dataset.replayid; | |
$(this).parent().append( $('#SRC_ChatLog') ); | |
if (cachedGames[gameId]) { | |
showChatLog(gameId, true); | |
} else { | |
getProcessAndShowReplay(gameId, true); | |
} | |
}); | |
$('#replays-app').on('click', '#SRC_ChatLog_Close', function() { | |
$('#SRC_ChatLog').fadeOut(100, function() { | |
$('#replays-app').append( $('#SRC_ChatLog') ); | |
}); | |
}); | |
$('#replays-app').css('position', 'relative').append('<div id="SRC_GameStats_Show" title="Show the server stats for the last 250 public games"><i class="fa-solid fa-server"></i></div><div id="SRC_GameStats_AddRows" title="Show the last 250 public games in the table"><i class="fa-solid fa-table-list"></i></div>'); | |
$('#SRC_GameStats_AddRows').hide(); | |
$('#SRC_GameStats_Show').on('click', function() { | |
if (initGameStatsNeeded) { | |
initGameStats(); | |
initGameStatsNeeded = false; | |
} | |
if (!jsonData.length || gameStatsLoadedDate < Date.now() - 60000) { | |
getGamesData(function(success) { | |
if (success) { | |
showGamesStats(); | |
} | |
}); | |
} else if (jsonData.length) { | |
showGamesStats(); | |
} | |
}); | |
$('#SRC_GameStats_AddRows').on('click', function() { | |
if (!jsonData.length || gameStatsLoadedDate < Date.now() - 60000) { | |
getGamesData(function(result) { | |
if (result) { | |
addExtraRowsToTable(); | |
} | |
}); | |
} else if (jsonData.length) { | |
addExtraRowsToTable(); | |
} | |
}); | |
$('#replays-app').on('click', '.SRC_Favorite', function() { | |
let isFavorite = !$(this).hasClass('fa-regular'); | |
let replayKey = this.dataset.key; | |
let element = $(this); | |
if (!replayKey) { | |
return; | |
} | |
if (isFavorite) { | |
$.post('/replays/favoriteReplay', 'key=' + replayKey).done(function() { | |
element.removeClass('fa').addClass('fa-regular').attr('title', 'Add to Favorites'); | |
}); | |
} else { | |
$.post('/replays/favoriteReplay', 'key=' + replayKey + '&set=true&name=¬es=').done(function() { | |
element.removeClass('fa-regular').addClass('fa').attr('title', 'Remove from Favorites'); | |
}); | |
} | |
}); | |
} | |
}); | |
function toSeconds(value) { | |
return value.split(':').reduce((acc, time) => (60 * acc) + +time); | |
} | |
function getYMDHOld(date, date2 = Date.now(), useShortFormat = false) { | |
let dateNew = dayjs(date2).diff(date, 'year', true).toFixed(1); | |
let isFuture = dateNew < 0; | |
dateNew = Math.abs(dateNew); | |
if (dateNew > 1) { | |
dateNew = (isFuture ? 'in ' : '') + dateNew + (useShortFormat ? 'y' : ' years' + (isFuture ? '' : ' old')); | |
} else { | |
let months = dayjs(date2).diff(date, 'month', true).toFixed(1); | |
isFuture = months < 0; | |
months = Math.abs(months); | |
if (months > 1) { | |
dateNew = (isFuture ? 'in ' : '') + months + (useShortFormat ? 'M' : ' months' + (isFuture ? '' : ' old')); | |
} else { | |
let days = dayjs(date2).diff(date, 'day', true).toFixed(1); | |
isFuture = days < 0; | |
days = Math.abs(days); | |
if (days > 1) { | |
dateNew = (isFuture ? 'in ' : '') + days + (useShortFormat ? 'd' : ' days' + (isFuture ? '' : ' old')); | |
} else { | |
let hours = dayjs(date2).diff(date, 'hour', true).toFixed(1); | |
isFuture = hours < 0; | |
hours = Math.abs(hours); | |
if (hours > 1) { | |
dateNew = (isFuture ? 'in ' : '') + hours + (useShortFormat ? 'h' : ' hours' + (isFuture ? '' : ' old')); | |
} else { | |
let minutes = dayjs(date2).diff(date, 'minutes', true).toFixed(1); | |
isFuture = minutes < 0; | |
minutes = Math.abs(minutes); | |
dateNew = (isFuture ? 'in ' : '') + minutes + (useShortFormat ? 'm' : ' minutes' + (isFuture ? '' : ' old')); | |
} | |
} | |
} | |
} | |
return dateNew; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment