Skip to content

Instantly share code, notes, and snippets.

@blackkorchid
Last active February 11, 2024 23:18
Buddy Wins With/Against TopLeft
// ==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