ToastPro

A script to go with your EggPro

// ==UserScript==
// @name         ToastPro
// @namespace    https://toastpro.herokuapp.com/
// @version      0.1.5
// @description  A script to go with your EggPro
// @author       Electro
// @match        *://*.koalabeast.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js
// @require      https://unpkg.com/[email protected]/lib/anime.min.js
// @icon         https://toastpro.subaverage.site/toast.png
// @supportURL   https://www.reddit.com/message/compose/?to=-Electron-
// @website      https://toastpro.subaverage.site
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
	'use strict';
	console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')');

	const TOAST_SERVER_HOST = window.TOAST_SERVER_HOST || "toastpro.subaverage.site";
	let wsocket = null;

	const CONSTANTS = {
		EMOTE_COUNT: 32,
		PING_LIFETIME: 2500,
		PING_IN_OUT_LIFETIME: 250,
		AUTHENTICATION_TIMEOUT: 30 * 24 * 60 * 60 * 1000,
		WEBSOCKET_TIMEOUT: 10000,
		EMOTES: {
			SIZE: 24,
			CROP_SIZE: 32
		}
	};
	const EVENTS = {
		AUTHENTICATE: 1,
		AUTHENTICATE_SUCCESS: 2,
		AUTHENTICATE_FAIL: 3,
		ENDORSEMENT: 4,
		POSITION_PING: 5,
		EMOTE: 6
	};
	let TOASTPRO_DATA = localStorage.getItem('TOASTPRO_DATA') || '{}';
	let TOASTPRO_AUTHENTICATED = false;
	TOASTPRO_DATA = JSON.parse(TOASTPRO_DATA);
	TOASTPRO_DATA = {
		LAST_PROFILE_RETRIEVAL: 0,
		EMOTES: [0, 0, 0, 0, 0, 0, 0, 0],
		PING_SOUND_VOLUME: 0.6,
		EMOTE_SOUND_VOLUME: 0.6,
		POSITION_PING_OPACITY: 0.85,
		EMOTE_OPACITY: 0.85,
		PING_FLAIR_SCALE: 1,
		...TOASTPRO_DATA
	};

	if(location.pathname.startsWith('/profile/')) {
		domReady(() => {
			$('head').append(`
<style>
#toastProModal p {
	margin: 0;
}

#selectableEmotes {
	width: 100%;
	padding: 0.5rem;
}

#selectableEmotes .emote {
	display: inline-block;
}

#selectableEmotes {
	display: inline-block;
}

#equippedEmotes .emote {
	transition: 0.25s all ease-in-out;
	position: relative;
	bottom: 0px;
}

.emote {
	width: 32px;
	height: 32px;
	/* background-size: 1264px 16px; */
	cursor: pointer;
	background-image: url(${'https://' + TOAST_SERVER_HOST}/emotes.png);
}

.emote.selected-emote {
	position: relative;
	bottom: 8px !important;
}

.emote-list {
	display: inline-flex;
}

#toastProModal .settings-table {
	width: 100%;
}
</style>
`);
			$(`
<div class="form-group">
	<label class="col-sm-4 control-label">ToastPro</label>
	<div class="col-sm-8 form-link">
		<a href="#" data-toggle="modal" data-target="#toastProModal">Configure your Toaster 🍞</a>
	</div>
</div>`).insertAfter($('#settings form .form-group').eq(6));
			$('body').append(`
<div class="modal fade" id="toastProModal" tabindex="-1" role="dialog" aria-labelledby="toastProModalLabel" style="display: none;">
	<div class="modal-dialog" role="document">
		<div class="modal-content">
			<div class="modal-header">
				<button type="button" class="btn btn-default close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
				<h4 class="modal-title" id="toastProModalLabel">ToastPro 🍞</h4>
			</div>
			<div class="modal-body">
				<div class="row">
					<div class="col text-center">
						<p class="m-0">Authentication Status: <span id="authenticationStatus">Loading...</span></p>
					</div>
				</div>
				<hr>

				<div class="row">
					<div class="col-sm-6">
						<p style="margin-bottom: 0.5rem;">Equipped Emotes:</p>
						<div id="equippedEmotes" class="emote-list">
							<div class="emote selected-emote" data-id="${TOASTPRO_DATA.EMOTES[0]}" data-index="0"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[1]}" data-index="1"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[2]}" data-index="2"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[3]}" data-index="3"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[4]}" data-index="4"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[5]}" data-index="5"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[6]}" data-index="6"></div>
							<div class="emote" data-id="${TOASTPRO_DATA.EMOTES[7]}" data-index="7"></div>
						</div>
					</div>
					<div class="col-sm-6">
						<div id="selectableEmotes" class="emote-list"></div>
					</div>
				</div>
				<hr>
				<div class="row">
					<div class="col-sm-12">
						<table class="settings-table">
							<tbody>
								<tr>
									<td data-label="PING_SOUND_VOLUME">Ping SFX Volume (<span>0</span>): </td>
									<td><input type="range" class="toast-setting" name="PING_SOUND_VOLUME" min="0" max="1" step="0.05"></td>
								</tr>
								<tr>
									<td data-label="EMOTE_SOUND_VOLUME">Emote SFX Volume (<span>0</span>): </td>
									<td><input type="range" class="toast-setting" name="EMOTE_SOUND_VOLUME" min="0" max="1" step="0.05"></td>
								</tr>
								<tr>
									<td data-label="POSITION_PING_OPACITY">Position Ping Opacity (<span>0</span>): </td>
									<td><input type="range" class="toast-setting" name="POSITION_PING_OPACITY" min="0" max="1" step="0.05"></td>
								</tr>
								<tr>
									<td data-label="EMOTE_OPACITY">Emote Opacity (<span>0</span>): </td>
									<td><input type="range" class="toast-setting" name="EMOTE_OPACITY" min="0" max="1" step="0.05"></td>
								</tr>
								<tr>
									<td data-label="PING_FLAIR_SCALE">Ping Flair Scale (<span>0</span>): </td>
									<td><input type="range" class="toast-setting" name="PING_FLAIR_SCALE" min="0.8" max="1.5" step="0.05"></td>
								</tr>
							</tbody>
						</table>
					</div>
				</div>
			</div>
			<div class="modal-footer">
				<p class="status-message" style="display: none;"></p>
				<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
				<!-- <button type="button" class="btn btn-primary" id="saveToastProSettings">Save changes</button> -->
			</div>
		</div>
	</div>
</div>
`);

			authenticate().then(() => {
				if(TOASTPRO_AUTHENTICATED) {
					$("#authenticationStatus").text("Authenticated!");
				} else {
					$("#authenticationStatus").text("Not Authenticated!");
				}
			});

			$('#saveToastProSettings').click(() => {
				$('#toastProModal .status-message').text("Saved settings!");
				$('#toastProModal .status-message').slideDown();
				setTimeout(() => $('#toastProModal .status-message').slideUp(), 2000);
			});

			$("#equippedEmotes .emote").click(function(e){
				$("#equippedEmotes .emote").removeClass("selected-emote");
				$(this).addClass("selected-emote");
			});

			$('#toastProModal .toast-setting').each(function(){
				if(this.type === 'range') {
					this.value = TOASTPRO_DATA[this.name];
					$(`[data-label="${this.name}"] span`).text(this.value);
				}
			});

			$('#toastProModal .toast-setting').on('input change', function(e) {
				let settingName = e.currentTarget.name;

				if(e.currentTarget.type === 'range') {
					TOASTPRO_DATA[settingName] = Number(e.currentTarget.value);
					$(`[data-label="${settingName}"] span`).text(TOASTPRO_DATA[settingName]);
				}

				saveToastProData();
			});

			for (let i = 0; i < CONSTANTS.EMOTE_COUNT; i++) {
				$("#selectableEmotes").append(`
				<div class="emote" data-id="${i}"></div>
			`);
			}

			$("#selectableEmotes .emote").click(function(e){
				if($(".selected-emote").length === 0) return;

				$(".selected-emote").attr("data-id", $(this).attr("data-id"));

				TOASTPRO_DATA.EMOTES[Number($(".selected-emote").attr("data-index"))] = Number($(".selected-emote").attr("data-id"));
				saveToastProData();

				updateEmoteIcons();
			});

			updateEmoteIcons();
		});
	} else if(location.pathname.startsWith('/groups/')) {
		authenticate().then(() => {
			if(TOASTPRO_AUTHENTICATED) {
				$("#authenticationStatus").text("Authenticated!");
			} else {
				$("#authenticationStatus").text("Not Authenticated!");
			}
		});
	} else if(location.pathname.startsWith('/game') && !location.pathname.startsWith('/games')) {
		let startedToast = false;
		domReady(() => tagpro.ready(() => {
			tagpro.socket.on('map', function(data) {
				if (data.info.name === "eggball"){
					startToastIngame();
				}
			});

			tagpro.socket.on("eggBall", function(data) {
				startToastIngame();
			});
		}));

		async function startToastIngame() {
			if(startedToast) return;
			startedToast = true;

			$('head').append(`<style>
		.game #mapInfo {
			margin-bottom: 0;
		}

		#toastInfo {
			margin-bottom: 10px;
		}

		#emoteWheel {
			width: 25vw;
			height: 25vh;
			position: absolute;
			left: 50%;
			top: 56%;
			-webkit-transform: translate(-50%, -50%);
			transform: translate(-50%, -50%);
			pointer-events: none;
		}
		#emoteWheel table {
			margin: 0 auto;
			border-collapse: inherit;
		}

		#emoteWheel td {
			width: 40px;
			height: 40px;
			border: 2px solid rgba(0, 0, 0, 0.8);
			border-radius: 50%;
			background-color: rgba(0, 0, 0, 0.3);
		}

		.emote-wheel-crop {
			width: 32px;
			height: 32px;
			margin: 0 auto;
			background-image: url(${'https://' + TOAST_SERVER_HOST}/emotes.png);
		}

		td.selected-emote {
			border: 2px solid rgba(139, 195, 74, 0.7) !important;
			background-color: rgba(139, 195, 74, 0.2) !important;
		}

		.mid-wheel {
			opacity: 0;
		}
		</style>`);

			$(`<div id="toastInfo">🍞 Toast Connecting...</div>`).insertAfter('#mapInfo');
			$('body').append(`
<div id="emoteWheel">
	<table>
		<tbody>
			<tr>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[0]}"></div></td>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[1]}"></div></td>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[2]}"></div></td>
			</tr>
			<tr>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[3]}"></div></td>
				<td class="mid-wheel"></td>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[4]}"></div></td>
			</tr>
			<tr>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[5]}"></div></td>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[6]}"></div></td>
				<td><div class="emote-wheel-crop" data-id="${TOASTPRO_DATA.EMOTES[7]}"></div></td>
			</tr>
		</tbody>
	</table>
</div>
`);

			$("#emoteWheel").hide();
			$(".emote-wheel-crop").each(function(idx){
				let xPos = $(this).attr("data-id") * 32;
				$(this).css("background-position", "-" + xPos + "px 0px");

				let cropParent = $(this).parent();
				cropParent.css("position", "relative");

				if(idx === 1){
					cropParent.css("top", "-20px");
				} else if(idx === 3){
					cropParent.css("left", "-20px");
				} else if(idx === 4){
					cropParent.css("right", "-20px");
				} else if(idx === 6){
					cropParent.css("bottom", "-20px");
				}
			});

			await new Promise(resolve => {
				let interval = setInterval(() => {
					if(tagpro.players[tagpro.playerId] || tagpro.spectator) {
						clearInterval(interval);
						resolve();
					}
				}, 250);
			});

			wsocket = new WebSocket('wss://' + TOAST_SERVER_HOST);
			const clientPlayer = tagpro.spectator ? null : tagpro.players[tagpro.playerId];
			let authFailed = false;
			let rightMouseDown = false;
			let rightMouseDownPoint = {x: 0, y: 0};
			let mousePos = {x: 0, y: 0};
			let endorsementInterval;
			let lastPingTime = Date.now();
			let positionPings = new Map();
			let selectedEmote = 0;
			let emoteBaseTexture = null;
			let pingSoundEffect = () => {
				let audio = new Audio('https://' + TOAST_SERVER_HOST + '/posping.mp3');
				audio.volume = TOASTPRO_DATA.PING_SOUND_VOLUME;
				return audio;
			}

			let emoteSoundEffect = () => {
				let audio = new Audio('https://' + TOAST_SERVER_HOST + '/select_emote.mp3');
				audio.volume = TOASTPRO_DATA.EMOTE_SOUND_VOLUME;
				return audio;
			}

			PIXI.Loader.shared.onComplete.once(function() {
				emoteBaseTexture = new PIXI.BaseTexture(PIXI.Loader.shared.resources.emotes.data);
			});

			PIXI.Loader.shared
				.add('ping-target', 'https://' + TOAST_SERVER_HOST + '/target.png')
				.add('emotes', 'https://' + TOAST_SERVER_HOST + '/emotes.png').load();

			wsocket.addEventListener('open', function() {
				if(clientPlayer) {
					wsocket.send(makePacket({
						event: EVENTS.AUTHENTICATE,
						data: { name: clientPlayer.name, token: Cookies.get('nekotizer') || "meow", gameID: tagproConfig.gameId }
					}));
				} else {
					wsocket.send(makePacket({
						event: EVENTS.AUTHENTICATE,
						data: { spectator: true, gameID: tagproConfig.gameId }
					}));
				}
			});

			wsocket.addEventListener('message', eventPacket => {
				const { event, data } = parse(eventPacket.data);

				if(event === EVENTS.AUTHENTICATE_SUCCESS) {
					$('#toastInfo').text('🍞 Toast Connected!');

					endorsementTick();
					endorsementInterval = setInterval(endorsementTick, 4000);
				} else if(event === EVENTS.AUTHENTICATE_FAIL) {
					$('#toastInfo').text('🍞 Toast Disconnected! ERRCODE: ' + data);
					authFailed = true;
				} else if(event === EVENTS.POSITION_PING) {
					createPositionalPing(getPlayerFromName(data[0]), data[1], data[2]);
				} else if(event === EVENTS.EMOTE) {
					showEmote(getPlayerFromName(data[0]), data[1]);
				}
			});

			wsocket.addEventListener('ping', heartbeat);
			wsocket.addEventListener('close', function clear() {
				clearTimeout(wsocket.pingTimeout);
				clearInterval(endorsementInterval);
				if(!authFailed) $('#toastInfo').text('🍞 Toast Disconnected! ERRCODE: TIMEOUT');
			});

			$('#viewport').on('mousemove', function(e) {
				mousePos = {x: e.offsetX, y: e.offsetY}
			});

			$('#viewport').on('mousedown contextmenu', function(e) {
				if(tagpro.spectator) return;
				if(e.type === "contextmenu"){
					e.preventDefault();
					return;
				}

				let isRightMB;
				e = e || window.event;
				if ("which" in e) { // Gecko (Firefox), WebKit (Safari/Chrome) & Opera
					isRightMB = e.which == 3;
				} else if ("button" in e) { // IE, Opera
					isRightMB = e.button == 2;
				}

				if(isRightMB) {
					e.preventDefault();

					const pos = {
						x: tagpro.players[tagpro.playerId].x + (mousePos.x - ($('#viewport').width() / 2)),
						y: tagpro.players[tagpro.playerId].y + (mousePos.y - ($('#viewport').height() / 2))
					};
					wsocket.send(makePacket({event: EVENTS.POSITION_PING, data: [Math.floor(pos.x), Math.floor(pos.y)]}));
				}
			});

			$('#viewport').on('mouseup', function(e) {
				if(tagpro.spectator) return;
				if(e.which === 3) {
					e.preventDefault();
					rightMouseDown = false;
					// $("#emoteWheel").hide();
				}
			});

			$(document).on('keydown', function(e) {
				if(tagpro.spectator) return;
				let emoteKeys = [49, 50, 51, 52, 53, 54, 55, 56];
				let selectedEmote = emoteKeys.indexOf(e.keyCode);

				if(selectedEmote !== -1) {
					wsocket.send(makePacket({event: EVENTS.EMOTE, data: TOASTPRO_DATA.EMOTES[selectedEmote]}));
				}
			});

			function endorsementTick() {
				const endorsementObject = makePlayerEndorsements();
				wsocket.send(makePacket({event: EVENTS.ENDORSEMENT, data: endorsementObject}));
			}

			function createPositionalPing(player, x, y) {
				if(!player) return;
				if(player.dead) return;

				let oldPosPing = positionPings.get(player.name);
				if(oldPosPing) {
					oldPosPing.tween.pause();
					oldPosPing.tween.seek(CONSTANTS.PING_LIFETIME + (CONSTANTS.PING_IN_OUT_LIFETIME * 2));
					oldPosPing.tween.play();
					positionPings.delete(player.name);
				}

				let pingContainer = new PIXI.Container();
				let pingOutlineSprite = new PIXI.Sprite.fromImage(PIXI.Loader.shared.resources['ping-target'].texture);
				let flairSprite = new PIXI.Sprite(player.sprites.flair.texture);

				pingContainer.addChild(pingOutlineSprite);
				pingContainer.addChild(flairSprite);

				flairSprite.x = 16;
				flairSprite.y = 16;
				flairSprite.anchor.x = 0.5;
				flairSprite.anchor.y = 0.5;
				flairSprite.width *= TOASTPRO_DATA.PING_FLAIR_SCALE;
				flairSprite.height *= TOASTPRO_DATA.PING_FLAIR_SCALE;

				pingOutlineSprite.tint = player.team === 1 ? 0xFF0000 : 0x0000FF;

				pingContainer.x = x + 19;
				pingContainer.y = y + 19;
				pingContainer.alpha = 0;
				pingContainer.pivot.x = pingContainer.width / 2
				pingContainer.pivot.y = pingContainer.height / 2
				pingContainer.rotation = Math.PI * 0.25;

				tagpro.renderer.layers.foreground.addChild(pingContainer);
				pingContainer.pingID = Math.floor(Math.random() * 1000);

				let tween = anime({
					targets: pingContainer,
					keyframes: [
						{ alpha: TOASTPRO_DATA.POSITION_PING_OPACITY, rotation: 0, easing: 'easeOutQuint', duration: CONSTANTS.PING_IN_OUT_LIFETIME },
						{ alpha: TOASTPRO_DATA.POSITION_PING_OPACITY, rotation: 0, delay: CONSTANTS.PING_LIFETIME },
						{ alpha: 0,    rotation: -Math.PI * 0.25, easing: 'easeOutQuint', delay: CONSTANTS.PING_IN_OUT_LIFETIME, duration: CONSTANTS.PING_IN_OUT_LIFETIME }
					],
					complete: (anim) => {
						let currentPosPing = positionPings.get(player.name);
						pingContainer.destroy({
							children: true
						});

						if(currentPosPing && currentPosPing.pingID === pingContainer.pingID) {
							positionPings.delete(player.name);
						}
					}
				});

				pingContainer.tween = tween;

				if(typeof player.pingSound === 'undefined') {
					player.pingSound = pingSoundEffect();
				}

				player.pingSound.play();

				positionPings.set(player.name, pingContainer);
			}

			function showEmote(player, emoteID) {
				if(!player) return;
				if(!emoteBaseTexture) return;
				if(player.dead) return;

				if(typeof player.sprites.emote === 'undefined') {
					player.sprites.emote = new PIXI.Sprite();
					player.sprites.ball.addChild(player.sprites.emote);
					player.sprites.emote.width = CONSTANTS.EMOTES.SIZE;
					player.sprites.emote.height = CONSTANTS.EMOTES.SIZE;
					player.sprites.emote.anchor.x = 0.5;
					player.sprites.emote.anchor.y = 0.5;
					player.sprites.emote.x = player.sprites.actualBall.width / 2;
					player.sprites.emote.y = player.sprites.actualBall.height / 2;
					player.sprites.emote.alpha = TOASTPRO_DATA.EMOTE_OPACITY;
				} else {
					player.sprites.emote.tween.pause();
					player.sprites.emote.tween = null;
				}

				let emoteTexture = new PIXI.Texture(
					emoteBaseTexture,
					new PIXI.Rectangle(
						emoteID * CONSTANTS.EMOTES.CROP_SIZE,
						0,
						CONSTANTS.EMOTES.CROP_SIZE,
						CONSTANTS.EMOTES.CROP_SIZE
					)
				);

				player.sprites.emote.texture = emoteTexture;

				player.sprites.emote.width = 0;
				player.sprites.emote.height = 0;

				player.sprites.emote.tween = anime({
					targets: player.sprites.emote,
					keyframes: [
						{ width: CONSTANTS.EMOTES.SIZE, height: CONSTANTS.EMOTES.SIZE, easing: 'easeOutBounce', duration: 200 },
						{ width: CONSTANTS.EMOTES.SIZE, height: CONSTANTS.EMOTES.SIZE, delay: 2000 },
						{ width: 0, height: 0, easing: 'easeOutBounce', duration: 200 }
					]
				});

				if(typeof player.emoteSound === 'undefined') {
					player.emoteSound = emoteSoundEffect();
				}

				player.emoteSound.play();
			}

			animate();
			function animate() {
				requestAnimationFrame(animate);

				if(rightMouseDown) {
					const angleSubDivision = (Math.PI * 2) / 8;
					const centerPoint = {x: $('#viewport').width() / 2, y: $('#viewport').height() / 2};
					const angle = angleFromPoints(centerPoint.x, centerPoint.y, mousePos.x, mousePos.y) + (angleSubDivision / 2);
					selectedEmote = (Math.floor(angle / angleSubDivision) + 4) % 8;

					$(".emote-wheel-crop").parent().removeClass("selected-emote");
					$(".emote-wheel-crop").eq(selectedEmote).parent().addClass("selected-emote");
				}
			}
		}
	}

	function getPlayerFromName(name) {
		let playerKeys = Object.keys(tagpro.players);

		for(const key of playerKeys) {
			if(tagpro.players[key].name.toUpperCase() === name.toUpperCase() && tagpro.players[key].auth) return tagpro.players[key];
		}

		return null;
	}

	function makePlayerEndorsements() {
		return Object.keys(tagpro.players).reduce((acc, playerID) => {
			const player = tagpro.players[playerID];
			if(!player.auth) return acc;
			acc[player.name] = {team: player.team};
			return acc;
		}, {});
	}

	function domReady(callback){
		// in case the document is already rendered
		if (document.readyState !== 'loading') callback();
		// modern browsers
		else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
		// IE <= 8
		else document.attachEvent('onreadystatechange', function(){
			if (document.readyState === 'complete') callback();
		});
	}

	function saveToastProData() {
		localStorage.setItem('TOASTPRO_DATA', JSON.stringify(TOASTPRO_DATA));
	}

	function makePacket({event, data}) {
		return JSON.stringify([event, data]);
	}

	function parse(data) {
		try {
			let parsed = JSON.parse(data);
			if(!Array.isArray(parsed)) throw new Error("Packet is not an array.");
			return {event: parsed[0], data: parsed[1]};
		} catch(e) {
			console.error("PACKET PARSE ERROR:", e);
			return {event: null, data: null};
		}
	}

	function updateEmoteIcons(){
		$(".emote").each(function(idx){
			let xPos = $(this).attr("data-id") * 32;
			$(this).css("background-position", "-" + xPos + "px 0px");
		});
	}

	function angleFromPoints(cx, cy, ex, ey) {
		let dy = ey - cy;
		let dx = ex - cx;
		let theta = Math.atan2(dy, dx); // range (-PI, PI]
		return theta;
	}

	async function authenticate() {
		if(Date.now() - TOASTPRO_DATA.LAST_PROFILE_RETRIEVAL > 0.0001 * 60000) {
			const tagproName = await getTagProName();
			let token = Cookies.get('nekotizer');

			TOASTPRO_DATA.LAST_PROFILE_RETRIEVAL = Date.now();
			saveToastProData();

			const isAuthJSON = await fetch('https://' + TOAST_SERVER_HOST + '/is_auth', {
				method: "POST",
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					name: tagproName,
					token: token || "meow"
				})
			}).then(r => r.json());
			if(isAuthJSON.error) return console.error("TOASTPRO: Failed to authenticate", isAuthJSON.error) || false;
			if(isAuthJSON.authenticated) {
				TOASTPRO_AUTHENTICATED = true;
				return true;
			}

			const response = await fetch('https://' + TOAST_SERVER_HOST + '/auth', {
				method: "POST",
				headers: {
					'Content-Type': 'application/json'
				},
				body: JSON.stringify({
					name: tagproName,
					token: token || "bark"
				})
			}).then(r => r.json());

			if(response.error) return console.error("TOASTPRO: Failed to authenticate", response.error) || false;

			Cookies.set('nekotizer', response.token, {
				expires: new Date(Date.now() + CONSTANTS.AUTHENTICATION_TIMEOUT),
				domain: '.koalabeast.com'
			});
			TOASTPRO_AUTHENTICATED = true;
		}
	}

	async function getTagProName() {
		const profileID = $('#profile-btn').attr('href').split('/').at(-1);
		const tagproProfile = await fetch('https://' + location.host + '/profiles/' + profileID).then(r => r.json());
		return tagproProfile[0].reservedName;
	}

	async function getTagProNameDEBUG() {
		return "Electro";
	}

	function heartbeat() {
		if(!wsocket) return;
		clearTimeout(wsocket.pingTimeout);

		// Use `WebSocket#terminate()`, which immediately destroys the connection,
		// instead of `WebSocket#close()`, which waits for the close timer.
		// Delay should be equal to the interval at which your server
		// sends out pings plus a conservative assumption of the latency.
		wsocket.pingTimeout = setTimeout(() => {
			if(!wsocket) return;
			wsocket.close();
		}, 10000 + 1000);
	}
})();