Last active
February 11, 2024 23:18
Buddy Wins With/Against TopLeft
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 Buddy Wins With/Against TopLeft | |
// @version 0.6 | |
// @description Display player names with buddy flair in top left corner | |
// @include https://tagpro.koalabeast.com/game | |
// @include https://tagpro.koalabeast.com/game?* | |
// @include http://tagpro-maptest.koalabeast.com:* | |
// @downloadURL https://gist.github.com/blackkorchid/2a8c2ee6e0e2d2f2d2072156bfce771f/raw/6f9c315e09cfb9be2efa53378557a54abcfdbbed/Buddy_Wins_With-Against_TopLeft.user.js | |
// @updateURL https://gist.github.com/blackkorchid/2a8c2ee6e0e2d2f2d2072156bfce771f/raw/6f9c315e09cfb9be2efa53378557a54abcfdbbed/Buddy_Wins_With-Against_TopLeft.user.js | |
// @grant GM_addStyle | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @author Maelstrom, with code from EphewSeakay and nabbynz | |
// ==/UserScript== | |
console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')'); | |
// Initialize a cache for openskill values | |
var openskillCache = null; | |
var preGameInterval = null; | |
var gameStarted = false; | |
var displayedPreGame = false; | |
var buddyWinsIgnoreSomeBalls = true; //ignore Some Balls [default: true] | |
var buddyWinsMinimumGameTime = 20; //minimum game time % before that player's data will save [default: 20] | |
var buddyWinsMinimumGames = 1; //number of games before the win % will show (0 for all) [default: 1] | |
var myTeamSprite; | |
var otherTeamSprite; | |
function ensureCacheLoaded() { | |
if (openskillCache === null) { // Cache not loaded yet | |
const cacheString = localStorage.getItem('openskillCache'); | |
try { | |
openskillCache = JSON.parse(cacheString) || {}; | |
} catch (e) { | |
console.error('Error parsing OpenSkill cache from local storage:', e); | |
openskillCache = {}; // Initialize with an empty object in case of error | |
} | |
} | |
} | |
function saveSkillRating(username, skillRating) { | |
const cacheEntry = { | |
skillRating: skillRating, | |
timestamp: Date.now() // Current time in milliseconds | |
}; | |
ensureCacheLoaded(); | |
openskillCache[username] = cacheEntry; | |
localStorage.setItem('openskillCache', JSON.stringify(openskillCache)); | |
} | |
function getLastUpdateTimestamp() { | |
const now = new Date(); | |
const minutes = now.getMinutes(); | |
const lastUpdateMinute = minutes - (minutes % 15); // Rounds down to the nearest multiple of 15 | |
// Create a new date object representing the last update time | |
const lastUpdateTime = new Date(now); | |
lastUpdateTime.setMinutes(lastUpdateMinute, 0, 0); // Set to the last update minute, 0 seconds, 0 milliseconds | |
return lastUpdateTime.getTime(); // Convert to timestamp | |
} | |
function shouldUpdateRating(cachedTimestamp) { | |
const lastUpdateTimestamp = getLastUpdateTimestamp(); | |
return cachedTimestamp < lastUpdateTimestamp; // True if cached data is before the last update period | |
} | |
function getSkillRating(username, isAuthenticated) { | |
ensureCacheLoaded(); | |
const cacheEntry = openskillCache[username]; | |
if (cacheEntry && !shouldUpdateRating(cacheEntry.timestamp)) { | |
return Promise.resolve(cacheEntry.skillRating); // Cached data is fresh, return it | |
} else { | |
// Data is old or missing, fetch new data and update cache | |
return fetchAndStoreOpenSkill(username, isAuthenticated).then(newSkillValue => { | |
return newSkillValue; | |
}); | |
} | |
} | |
// Fetch OpenSkill value with caching | |
async function fetchAndStoreOpenSkill(username, isAuthenticated) { | |
// Construct the URL based on whether the player is authenticated | |
let url = `https://tagpro.dev/api/pub/openskill/${encodeURIComponent(username)}`; | |
if (isAuthenticated) { | |
url += "?auth=true"; | |
} | |
try { | |
const response = await fetch(url); | |
if (!response.ok) throw new Error('Network response was not ok'); | |
const data = await response.json(); | |
saveSkillRating(username,parseFloat(data.openskill)); | |
return parseFloat(data.openskill); | |
} catch (error) { | |
console.error('Error fetching open skill for', username, ':', error); | |
return null; | |
} | |
} | |
function checkAndUpdatePlayerSkills() { | |
const minutes = new Date().getMinutes(); | |
const updateTimes = [1, 16, 31, 46]; | |
if( updateTimes.includes(minutes) ) { | |
updateAllPlayerSkills(); | |
} | |
} | |
function updateAllPlayerSkills() { | |
let promises = []; | |
for (let playerId in tagpro.players) { | |
let player = tagpro.players[playerId]; | |
let promise = getSkillRating(player.name, player.auth); | |
promises.push(promise); | |
} | |
Promise.all(promises).then(results => { | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
}).catch(error => { | |
console.error("An error occurred while updating player skills:", error); | |
}); | |
} | |
async function fetchPlayerSkills(players) { | |
// Collect all promises for fetching player skills | |
const skillPromises = players.map(player => getSkillRating(player.name, player.auth)); | |
// Wait for all promises to resolve | |
const skills = await Promise.all(skillPromises.map(promise => promise.catch(e => null))); // Catch errors individually to avoid one failure failing all | |
// Return skills with player info | |
const playersWithSkills = players.map((player, index) => ({ | |
...player, | |
skill: skills[index] || 0 | |
})); | |
// Sort players by skill in descending order | |
playersWithSkills.sort((a, b) => b.skill - a.skill); | |
return playersWithSkills; | |
} | |
async function displayBuddyWins() { | |
if (tagpro.players[tagpro.playerId] ) | |
{ | |
tagpro.renderer.layers.ui.addChild(myTeamSprite); | |
tagpro.renderer.layers.ui.addChild(otherTeamSprite); | |
} | |
} | |
async function prepareBuddyWins() { | |
if (tagpro.players[tagpro.playerId] ) | |
{ | |
if (myTeamSprite && myTeamSprite.parent) { | |
myTeamSprite.parent.removeChild(myTeamSprite); | |
myTeamSprite = null; | |
} | |
if (otherTeamSprite && otherTeamSprite.parent) { | |
otherTeamSprite.parent.removeChild(otherTeamSprite); | |
otherTeamSprite = null; | |
} | |
let buddyPlayers = GM_getValue('buddyPlayers', {}); | |
let getBuddyFlair = function(winsWith, winsAgainst) { | |
//Happy to change these icons if someone can make them better :) | |
if (!winsWith || !winsAgainst) return ''; | |
else if (winsWith >= 60 && winsAgainst >= 60) return '👍'; //high win with, high win against - you are a better player than them 💜🚑👍✌ | |
else if (winsWith >= 60 && winsAgainst <= 40) return '🔥'; //high win with, low win against - they are a better player than you 🔥⚡ | |
else if (winsWith <= 40 && winsAgainst <= 40) return '☢️'; //low win with, low win against - they play better against you (you suck when you're on the same team) ☢️ | |
else if (winsWith <= 40 && winsAgainst >= 60) return '👎'; //low win with, high win against - you play better against them (they suck) 🤔👎 | |
else return ''; //😐 | |
}; | |
var myTeamNamesWithflair = "\nMy Team:"; | |
var myTeamWinPct; | |
var otherTeamNamesWithflair = "\n\n\n\n\n\nOther Team:"; | |
var otherTeamWinPct; | |
var myTeamColor; | |
var otherTeamColor; | |
function getTeamPlayers( team ) { | |
let players = []; | |
for (var playerId in tagpro.players) { | |
if ( (tagpro.players.hasOwnProperty(playerId)) && tagpro.players[playerId].name.substring(0, 9) != "Some Ball" && tagpro.players[playerId].team == team) { | |
players.push(tagpro.players[playerId]); | |
} | |
} | |
return players; | |
} | |
function calculateAverageSkill(playersWithSkill) { | |
let sumSkill = 0; | |
let count = 0; | |
playersWithSkill.forEach(playerWithSkill => { | |
sumSkill += playerWithSkill.skill; | |
count++ | |
}); | |
// Calculate average skill, ensuring we don't divide by zero | |
let averageSkill = count > 0 ? sumSkill / count : 0; | |
return averageSkill.toFixed(2); | |
} | |
let myTeam = tagpro.players[tagpro.playerId].team; | |
let otherTeam = 1; | |
if( myTeam === 1 ) { | |
otherTeam = 2; | |
myTeamColor = "#fab3b3"; | |
otherTeamColor = "#b7e8ff"; | |
} else { | |
myTeamColor = "#b7e8ff"; | |
otherTeamColor = "#fab3b3"; | |
} | |
if (!myTeamSprite) { | |
myTeamSprite = new PIXI.Text( | |
'', | |
{ | |
fontSize: "8pt", | |
strokeThickness: 3, | |
fill: myTeamColor, | |
fontWeight: "bold", | |
} | |
); | |
myTeamSprite.x = 10; | |
myTeamSprite.y = 30; | |
} | |
if (!otherTeamSprite) { | |
otherTeamSprite = new PIXI.Text( | |
'', | |
{ | |
fontSize: "8pt", | |
strokeThickness: 3, | |
fill: otherTeamColor, | |
fontWeight: "bold", | |
} | |
); | |
otherTeamSprite.x = 10; | |
otherTeamSprite.y = 30; | |
} | |
let myTeamPlayers = getTeamPlayers( myTeam ); | |
let myTeamPlayersWithSkills = await fetchPlayerSkills(myTeamPlayers); | |
let myTeamSkill = calculateAverageSkill( myTeamPlayersWithSkills ); | |
myTeamNamesWithflair += " " + myTeamSkill; | |
let otherTeamPlayers = getTeamPlayers( otherTeam ); | |
let otherTeamPlayersWithSkills = await fetchPlayerSkills(otherTeamPlayers); | |
let otherTeamSkill = calculateAverageSkill( otherTeamPlayersWithSkills ); | |
otherTeamNamesWithflair += " " + otherTeamSkill; | |
myTeamPlayersWithSkills.forEach( myTeamPlayer => { | |
let name = myTeamPlayer.name; | |
let openSkillText = ""; | |
let openskill = myTeamPlayer.skill; | |
if (openskill !== undefined && openskill !== null ) { | |
openSkillText = ` (${openskill.toFixed(2)})`; | |
if (name && buddyPlayers.hasOwnProperty(name) ) { | |
let winsWithPC = buddyPlayers[name].winsWith / (buddyPlayers[name].gamesWith || 1) * 100; | |
let winsAgainstPC = buddyPlayers[name].winsAgainst / (buddyPlayers[name].gamesAgainst || 1) * 100; | |
let games = buddyPlayers[name].gamesWith; | |
if (games > buddyWinsMinimumGames) { | |
let flair = getBuddyFlair(winsWithPC, winsAgainstPC); | |
myTeamWinPct = buddyPlayers[name].winsWith > 0 ? winsWithPC.toFixed(0) + '%' : '-'; | |
//let title = 'Won ' + buddyPlayers[name].winsWith + ' / ' + buddyPlayers[name].gamesWith + ' Games With (' + winsWithPC.toFixed(0) + '%)\nWon ' + buddyPlayers[name].winsAgainst + ' / ' + buddyPlayers[name].gamesAgainst + ' Games Against (' + winsAgainstPC.toFixed(0) + '%)'; | |
myTeamNamesWithflair += "\n" + flair + " " + name + " " + myTeamWinPct + openSkillText; | |
} | |
else { | |
myTeamNamesWithflair += "\n" + name + openSkillText; | |
} | |
} | |
else | |
{ | |
myTeamNamesWithflair += "\n" + name + openSkillText; | |
} | |
myTeamSprite.text = myTeamNamesWithflair; | |
} | |
}); | |
otherTeamPlayersWithSkills.forEach( otherTeamPlayer => { | |
let name = otherTeamPlayer.name; | |
let openSkillText = ""; | |
let openskill = otherTeamPlayer.skill; | |
if (openskill !== undefined && openskill !== null) { | |
openSkillText = ` (${openskill.toFixed(2)})`; | |
} | |
if (name && buddyPlayers.hasOwnProperty(name) ) { //gamepid is a check for when players have the same name | |
let winsWithPC = buddyPlayers[name].winsWith / (buddyPlayers[name].gamesWith || 1) * 100; | |
let winsAgainstPC = buddyPlayers[name].winsAgainst / (buddyPlayers[name].gamesAgainst || 1) * 100; | |
let games = buddyPlayers[name].gamesAgainst; | |
if (games > buddyWinsMinimumGames) { | |
let flair = getBuddyFlair(winsWithPC, winsAgainstPC); | |
otherTeamWinPct =buddyPlayers[name].winsAgainst > 0 ? winsAgainstPC.toFixed(0) + '%' : '-'; | |
//let title = 'Won ' + buddyPlayers[name].winsWith + ' / ' + buddyPlayers[name].gamesWith + ' Games With (' + winsWithPC.toFixed(0) + '%)\nWon ' + buddyPlayers[name].winsAgainst + ' / ' + buddyPlayers[name].gamesAgainst + ' Games Against (' + winsAgainstPC.toFixed(0) + '%)'; | |
otherTeamNamesWithflair += "\n" + flair + " " + name + " " + otherTeamWinPct + openSkillText; | |
} | |
else { | |
otherTeamNamesWithflair += "\n" + name + openSkillText; | |
} | |
} | |
else { | |
otherTeamNamesWithflair += "\n" + name + openSkillText; | |
} | |
otherTeamSprite.text = otherTeamNamesWithflair; | |
}); | |
} | |
}; | |
tagpro.ready(function() { | |
if( tagpro.state ) { | |
var fullGameLength; | |
var endTime; | |
let allPlayers = new Map(); | |
let clearable_TeamStats; | |
let trackPlayers = function() { | |
let now = Date.now(); | |
for (const id in tagpro.players) { | |
if (!tagpro.players.hasOwnProperty(id)) { | |
console.log('YAY SAVED A BUG IN NSE :)'); //this never seems to occur - please lmk if it does! | |
continue; | |
} | |
let tpP = tagpro.players[id]; | |
let value = { | |
'firstseen': allPlayers.has(+id) ? allPlayers.get(+id).firstseen : now, | |
'lastseen': now, //to track leavers (but only the last time someone leaves) | |
'name': tpP.name, //for name changes | |
'team': tpP.team, //for switches | |
'degree': tpP.degree, //for total team degrees | |
'score': tpP['score'], | |
's-tags': tpP['s-tags'], | |
's-pops': tpP['s-pops'], | |
's-grabs': tpP['s-grabs'], | |
's-drops': tpP['s-drops'], | |
's-hold': tpP['s-hold'], | |
's-captures': tpP['s-captures'], | |
's-prevent': tpP['s-prevent'], | |
's-returns': tpP['s-returns'], | |
's-support': tpP['s-support'], | |
's-powerups': tpP['s-powerups'] | |
}; | |
allPlayers.set(+id, value); | |
} | |
}; | |
let updateBuddyWins = function() { | |
let myTeam = tagpro.players[tagpro.playerId].team; | |
let isWin = (myTeam === 1 && tagpro.score.r > tagpro.score.b) || (myTeam === 2 && tagpro.score.b > tagpro.score.r); | |
for (const [id, player] of allPlayers) { | |
let saveThisPlayer = tagpro.players.hasOwnProperty(id) && tagpro.playerId !== id && (!buddyWinsIgnoreSomeBalls || buddyWinsIgnoreSomeBalls && !player.name.startsWith('Some Ball ')); //skip us; only save authenticated; skip Some Balls | |
if (saveThisPlayer) { | |
let timePlayed = Math.min(Math.round(((player.lastseen - player.firstseen) / fullGameLength / 1000) * 100), 100); | |
if (timePlayed > buddyWinsMinimumGameTime) { //minimum % of game time | |
if (!buddyPlayers.hasOwnProperty(player.name)) buddyPlayers[player.name] = { gamepid:0, gamesWith:0, winsWith:0, gamesAgainst:0, winsAgainst:0 }; | |
buddyPlayers[player.name].gamepid = id; | |
if (player.team === myTeam) { | |
buddyPlayers[player.name].gamesWith++; | |
if (isWin) buddyPlayers[player.name].winsWith++; | |
} else { | |
buddyPlayers[player.name].gamesAgainst++; | |
if (isWin) buddyPlayers[player.name].winsAgainst++; | |
} | |
} | |
} | |
} | |
GM_setValue('buddyPlayers', buddyPlayers); | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
}; | |
tagpro.socket.on('chat', function(data) { | |
if ((data.from === null) /*&& (tagpro.state === 1 || tagpro.state === 3)*/ ) { //system message | |
if (data.message.indexOf('has joined the') >= 0) { //update balls when a player joins and when you join a game already in progress | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
} else if (data.message.indexOf('has left the') >= 0) { // update balls when a player leaves | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
} | |
} | |
}); | |
let entryTime, joinTime, startTime, gameEndsAt; | |
let gameLengthMins = 6; //Assume a 6 min pub game. Will change if in a group. | |
let gameLengthSecs = gameLengthMins * 60; | |
let gameLengthMs = gameLengthSecs * 1000; | |
tagpro.socket.on('time', function(data) { | |
// data.states | |
// 1: Indicates the game is active. Players can move, score, etc. | |
// 2: The game has ended. This is not set in response to a time event sent by the game socket, but instead set client-side in response to the end event. | |
// 3: The game has not yet started. | |
// 5: The game is in overtime. | |
if (!startTime && data.state !== 3) { //if state === 3 then tagpro.gameEndsAt is when the pre-game period ends | |
if (tagpro.gameEndsAt) gameEndsAt = (new Date(tagpro.gameEndsAt)).getTime(); //expected end of normal game time | |
else if (tagpro.overtimeStartedAt) gameEndsAt = (new Date(tagpro.overtimeStartedAt)).getTime(); | |
else gameEndsAt = 0; | |
if (gameEndsAt) startTime = gameEndsAt - gameLengthMs; | |
} | |
//start tracking players... | |
if ((data.state === 1 || data.state === 3 || data.state === 5) && !clearable_TeamStats) { | |
trackPlayers(); | |
clearable_TeamStats = setInterval(function() { | |
if (data.state === 1 || data.state === 3 || data.state === 5) { | |
trackPlayers(); | |
} | |
}, 1000); | |
} | |
if (data.state === 3) { //before the actual start | |
entryTime = Date.now(); | |
joinTime = entryTime; | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
} else if (data.state === 1) { //game has started | |
if( !tagpro.spectator ) { | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
} | |
if (joinTime) { | |
entryTime = startTime; | |
joinTime = entryTime; | |
} else { | |
entryTime = Date.now(); //time we joined (mid-game) | |
joinTime = entryTime; | |
} | |
} else if (data.state === 5) { //overtime | |
if (!joinTime) { //joined in overtime | |
if( !tagpro.spectator ) { | |
prepareBuddyWins().then(() => { | |
displayBuddyWins(); | |
}); | |
} | |
entryTime = Date.now(); | |
joinTime = entryTime; | |
} | |
} | |
}); | |
tagpro.socket.on('end', function() { | |
clearInterval(clearable_TeamStats); | |
endTime = Date.now(); //actual end of game time | |
fullGameLength = (endTime - startTime) / 1000; //how long the whole game lasted (with or without us) | |
setTimeout(function() { | |
trackPlayers(); | |
if (!tagpro.spectator) updateBuddyWins(); | |
}, 365); | |
}); | |
} | |
// Check the time every minute and update player skills if an update time has passed (15, 30, 45, 60) | |
setInterval(checkAndUpdatePlayerSkills, 60000); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment