您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); } })();