NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name HaxBall TC Nerds script // @namespace http://tampermonkey.net/ // @version 0.3 // @description try to take over the world! // @author MaddoHatto // @source https://github.com/MaddoHatto/HaxBall-TC-Nerds-Script // @match https://www.haxball.com/headless // @require https://code.jquery.com/jquery-1.12.4.min.js // @grant none // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // @updateURL https://openuserjs.org/meta/MaddoHatto/HaxBall_TC_Nerds_script.meta.js // ==/UserScript== const RED_TEAM_ID = 1; const BLUE_TEAM_ID = 2; const BACKEND_BASE_URL = 'https://hax.opac.pl/'; const BALL_RADIUS = 10; const PLAYER_RADIUS = 15; const DISC_BALL_ID = 0; const OFFSIDE_AVATAR = '🔥'; const SAVE_REPLAY_BUTTON_ID = 'SAVE_REPLAY_BUTTON_ID'; const HOST_HANDICAP = '40'; const playersAvatars = { MaddoHatto: '🐻', "Nelson Mandela": 'xD', Amman: '🐺', ToPP: '🤡', hubigz: 'H', adamaru: '😈', rybak: '🧟♂️', panda: '🐼' } class HaxBallController { constructor() { this.touchingTheBallTimestamps = {}; this.ballSpeed = 0; this.prevBallPosition = null; this.goals = []; this.gameStartTimestamp = null; this.gameEndTimestamp = null; this.isPaused = false; this.isOffsideActive = false; this.votesForUnpause = {}; this.tick = 0; this.logBallSpeed = false; this.logPlayerPosition = false; this.playersOffsidePosition = { [RED_TEAM_ID]: {}, [BLUE_TEAM_ID]: {}, }; this.playersInitPosition = { [RED_TEAM_ID]: {}, [BLUE_TEAM_ID]: {}, }; this.initXLine = { [RED_TEAM_ID]: 0, [BLUE_TEAM_ID]: 0, } this.client = new Client(); this.gamePageController = null; this.matchResult = null; } initRoom() { this.room = window.HBInit({ roomName: "TC_NERDS_ROOM", password: '1', maxPlayers: 16, noPlayer: true // Remove host player (recommended!) }); this.room.setDefaultStadium("Rounded"); this.room.setScoreLimit(10); this.room.setTimeLimit(10); window.hbRoom = this.room; window.hbController = this; return this; } initListeners() { this.room.onPlayerChat = this.onPlayerChat.bind(this); this.room.onGameTick = this.onGameTick.bind(this); this.room.onGameStart = this.onGameStart.bind(this); this.room.onPlayerJoin = this.onPlayerJoin.bind(this); this.room.onPlayerLeave = this.onPlayerLeave.bind(this); this.room.onTeamGoal = this.onTeamGoal.bind(this); this.room.onTeamVictory = this.onTeamVictory.bind(this); this.room.onPlayerBallKick = this.onPlayerBallKick.bind(this); this.room.onGamePause = this.onGamePause.bind(this); this.room.onGameUnpause = this.onGameUnpause.bind(this); this.room.onPositionsReset = this.onPositionsReset.bind(this); return this; } initUserInterface() { try { document.body.style.background = '#939e7f url("https://www.haxball.com/hiF05fAx/__cache_static__/g/images/bg.png") fixed'; this.waitForRoomLinkElement(() => { let button = document.createElement("button"); button.innerHTML = "PLAY"; button.onclick = this.goToGameTab.bind(this); button.style.color = '#fff'; button.style.height = '100px'; button.style.width = '450px'; button.style.position = 'fixed'; button.style.top = '150px'; button.style.left = '10px'; button.style.fontSize = '36px'; button.style.background = 'linear-gradient(#8da86b, #658d59)'; button.style.border = '4px solid white'; button.style.borderRadius = '50px'; button.style.cursor = 'pointer'; button = document.body.appendChild(button); }); } catch (error) { console.log(error); } return this; } onPositionsReset() { this.clearPlayersOffsidePosition(); this.updateIsOffsideActive(false); } onPlayerChat(player, message) { if (this.isPauseCommand(message)) { return this.handlePauseCommand(player, message); } if (this.isFindTeamsCommand(message)) { return this.handleFindTeamsCommand(); } if (this.isVoteForUnpauseCommand(message)) { return this.handleVoteForUnpauseCommand(player); } return true; } onGameTick() { this.updateTouchingTheBall(); this.updateBallSpeed(); this.updateBallPosition(); this.tick++; } onGameStart() { this.updateGameStartTimestamp(); this.updateInitPlayerPositions(); } onPlayerJoin(player) { this.updateAdmins(); this.resetPlayerAvatar(player); } onPlayerLeave() { this.updateAdmins(); } onTeamGoal(teamId) { this.updateScorers(teamId); this.clearPlayersOffsidePosition(); } onTeamVictory(scores) { this.updateGameEndTimestamp(); this.matchResult = this.getMatchResult(scores); console.log('MATCH RESULT = ', this.matchResult); try { if (this.gamePageController) { setTimeout(() => { //const shouldSave = this.gamePageController.showConfirmModal('Save replay?'); //if (shouldSave) { // this.saveMatchResult(); //} }, 5000); } this.addSaveReplayButton(); } catch(error) { console.log(error); } this.clear(); } onPlayerBallKick(player) { this.updatePlayerTochedTheBall(player); this.updatePlayersOffsidePosition(player); this.updateIsOffsideActive(true); } onGamePause() { this.isPaused = true; } onGameUnpause() { this.isPaused = false; } getRoom() { return this.room; } getMatchResult(scores) { return { score: { Blue: scores.blue, Red: scores.red }, teams: this.getTeams(), goals: this.goals, startTimestamp: this.gameStartTimestamp, endTimestamp: this.gameEndTimestamp, duration: scores.time, rawPositionsAtEnd: this.getRawPositionsAtEnd() }; } getTeams() { const players = this.room.getPlayerList(); const result = { Red: [], Blue: [], Spectators: [] }; for (let i = 0; i < players.length; i++) { const player = players[i]; const teamName = this.getTeamName(player.team); if (result[teamName]) { result[teamName].push(player.name); } } return result; } getPlayers() { const players = this.room.getPlayerList(); const result = []; for (let i = 0; i < players.length; i++) { const player = players[i]; if (player.team === RED_TEAM_ID || player.team === BLUE_TEAM_ID) { result.push(player); } } return result; } getTeamName(teamId) { if (teamId === RED_TEAM_ID) { return 'Red'; } if (teamId === BLUE_TEAM_ID) { return 'Blue'; } return 'Spectators'; } getEnemyTeamId(teamId) { return teamId === RED_TEAM_ID ? BLUE_TEAM_ID : RED_TEAM_ID; } getRawPositionsAtEnd() { let result = ''; const players = this.getPlayers(); for (let i = 0; i < players.length; i++) { const player = players[i]; if (player) { const { x, y } = player.position; result = `${result}${x}--${y}|`; } } return result; } updateAdmins() { const players = this.room.getPlayerList(); if ( players.length == 0 ) return; // No players left, do nothing. if ( players.find((player) => player.admin) != null ) return; // There's an admin left so do nothing. this.room.setPlayerAdmin(players[0].id, true); // Give admin to the first non admin player in the list setTimeout(() => { this.setHostHandicap(); }, 3000); } updateGameStartTimestamp() { this.gameStartTimestamp = new Date().valueOf(); } updateGameEndTimestamp() { this.gameEndTimestamp = new Date().valueOf(); } updateBallSpeed() { if (this.prevBallPosition) { const currentBallPosition = this.room.getBallPosition(); if (currentBallPosition) { const vector = Math.sqrt(Math.pow(this.prevBallPosition.x - currentBallPosition.x, 2) + Math.pow(this.prevBallPosition.y - currentBallPosition.y, 2)); const speed = vector * 60; // game tick is 1/60 of second this.ballSpeed = (parseFloat((speed / 100).toFixed(2)) * 3600) / 1000; // km/h } } if (this.logBallSpeed && this.tick % 10 === 0) { console.log('Ball speed: ' + this.ballSpeed + 'km/h'); } } updateBallPosition() { this.prevBallPosition = this.room.getBallPosition(); } updateInitPlayerPositions() { const players = this.getPlayers(); for (let i = 0; i < players.length; i++) { const player = players[i]; this.playersInitPosition[player.team][player.id] = player.position; this.initXLine[player.team] = player.position.x; } } updateTouchingTheBall() { const players = this.getPlayers(); const ballPosition = this.room.getBallPosition(); const ballRadius = 10; const playerRadius = 15; const triggerDistance = ballRadius + playerRadius + 0.01; const timestamp = new Date().valueOf(); for (let i = 0; i < players.length; i++) { const player = players[i]; if ( player.position == null ) continue; // Skip players that don't have a position const distanceToBall = this.pointDistance(player.position, ballPosition); if ( distanceToBall < triggerDistance ) { this.touchingTheBallTimestamps[player.id] = timestamp; if (this.playersOffsidePosition[player.team][player.id]) { this.handleOffside(player); } else { this.clearPlayersOffsidePosition(player.team === RED_TEAM_ID ? BLUE_TEAM_ID : RED_TEAM_ID); } } if (this.logPlayerPosition && this.tick % 10 === 0) { console.log('Player ' + player.name + ' position: x = ' + player.position.x + ', y =' + player.position.y); } } } updatePlayersOffsidePosition(kicker) { const kickerTeamId = kicker.team; if (!this.isOffsideActive) { return; } if (this.playersOffsidePosition[kickerTeamId][kicker.id]) { this.handleOffside(kicker); return; } const playerList = this.getPlayers(); const ballPosition = this.room.getBallPosition(); const ballOffset = kickerTeamId === RED_TEAM_ID ? BALL_RADIUS : -BALL_RADIUS; const playerOffset = kickerTeamId === RED_TEAM_ID ? PLAYER_RADIUS : -PLAYER_RADIUS; let offsideLine = ballPosition.x + ballOffset; this.clearPlayersOffsidePosition(); const kickerTeam = []; for (let i = 0; i < playerList.length; i++) { const player = playerList[i]; if (player.id === kicker.id) { continue; } if (player.team === kickerTeamId) { kickerTeam.push(player); } else if (player.position !== null) { const position = player.position.x + playerOffset; if ( (kickerTeamId === RED_TEAM_ID && offsideLine < position) || (kickerTeamId === BLUE_TEAM_ID && offsideLine > position) ) { offsideLine = position } } } for(let i = 0; i < kickerTeam.length; i++) { const player = kickerTeam[i]; if (player.position !== null) { const position = player.position.x + playerOffset; if ( (kickerTeamId === RED_TEAM_ID && offsideLine < position && this.initXLine[RED_TEAM_ID] < position) || (kickerTeamId === BLUE_TEAM_ID && offsideLine > position && this.initXLine[BLUE_TEAM_ID] > position) ) { this.playersOffsidePosition[kickerTeamId][player.id] = player.name; } } } const offsidePlayerIds = Object.keys(this.playersOffsidePosition[kickerTeamId]); for(let i = 0; i < offsidePlayerIds.length; i++) { const playerId = offsidePlayerIds[i]; this.room.sendAnnouncement('Player ' + this.playersOffsidePosition[kickerTeamId][playerId] + ' is offside', null, 0xFFFFFF, null, 0); console.log(`PLAYER ${playerId} AVATAR ${OFFSIDE_AVATAR}`); this.room.setPlayerAvatar(playerId, OFFSIDE_AVATAR); } } updatePlayerTochedTheBall(player) { this.touchingTheBallTimestamps[player.id] = new Date().valueOf(); } updateScorers(teamId) { const players = this.getPlayers(); let scorerId = null; let scorerName = 'Unknown'; let closestTimestamp = null; for (let i = 0; i < players.length; i++) { const player = players[i]; if (player.team === teamId) { const playerTimestamp = this.touchingTheBallTimestamps[player.id]; if (playerTimestamp && (closestTimestamp === null || closestTimestamp < playerTimestamp)) { closestTimestamp = playerTimestamp; scorerId = player.id; scorerName = player.name; } } } this.room.sendAnnouncement('Goal scored by ' + scorerName, null, 0x00FF00, "bold", 2); this.addGoal(scorerName, teamId); this.clearTouchingTheBallTimestamp(); } updateIsOffsideActive(value) { this.isOffsideActive = value; } handleOffside(player) { this.room.pauseGame(true); this.room.sendAnnouncement(this.getTeamName(player.team) + ' team offside', null, 0xFFFFFF, "bold", 2); const players = this.getPlayers(); const offsideTeamId = player.team; const enemyTeamId = this.getEnemyTeamId(offsideTeamId); const playerDiscProperties = this.room.getPlayerDiscProperties(player.id); this.resetTeamToInitPosition(); const initXLine = this.initXLine[enemyTeamId]; const offset = enemyTeamId === RED_TEAM_ID ? 100 : -100; this.room.setDiscProperties(DISC_BALL_ID, { x: initXLine + offset, y: 0, xspeed: 0, yspeed: 0, }); this.clearPlayersOffsidePosition(); this.room.pauseGame(false); this.updateIsOffsideActive(false); } handlePauseCommand(player, message) { this.room.pauseGame(true); this.room.sendAnnouncement('Game paused by ' + player.name, null, 0x00FF00, "bold", 2); this.votesForUnpause = {}; return false; } handleFindTeamsCommand() { const playerList = this.room.getPlayerList(); if (playerList.length % 2 != 0) { this.room.sendAnnouncement('You have to have even amount of players!', null, 0xFF0000 , "bold", 2); return; } this.client.getCalculatedTeams(playerList, this.handleCalculatedTeams.bind(this)); return false; } handleCalculatedTeams(red, blue) { let firstRow = "Hi there! As Official Haxball's Scripted Referee I suggest these teams for tonight's skirmish:"; let secondRow = "On left side, in red uniforms:"; let thirdRow = "On right side, wearing blue: " for (let i = 0; i < red.length; i++) { secondRow += " @" + red[i]; thirdRow += " @" + blue[i]; } this.room.sendAnnouncement(firstRow); this.room.sendAnnouncement(secondRow, null, 0xE54141); this.room.sendAnnouncement(thirdRow, null, 0x5DADE2); } handleVoteForUnpauseCommand(player) { // check if already voted if (this.votesForUnpause[player.id]) { return; } this.votesForUnpause[player.id] = true; const playerList = this.getPlayers(); const playersCount = playerList.length; let votesCount = 0; for (let i = 0; i < playerList.length; i++) { const player = playerList[i]; if (this.votesForUnpause[player.id]) { votesCount++; } } if (votesCount === playersCount) { this.room.sendAnnouncement('All players voted to unpause!', null, 0x00FF00, "bold", 2); this.room.pauseGame(false); } else { this.room.sendAnnouncement('Player ' + player.name + ' voted to unpause (' + votesCount + '/' + playersCount + ')', null, 0x0FFC107, "bold", 2); } } handlePostMatchResult(data) { console.log(data); } isPauseCommand(message) { const commands = ['p', 'pp', 'ppp', 'pauza']; const trimmedMessage = message.trim(); return commands.indexOf(trimmedMessage) !== -1 && !this.isPaused; } isFindTeamsCommand(message) { const commands = ['find-teams']; const trimmedMessage = message.trim(); return commands.indexOf(trimmedMessage) !== -1; } isVoteForUnpauseCommand(message) { const commands = ['go', 'rdy']; const trimmedMessage = message.trim(); return this.isPaused && commands.indexOf(trimmedMessage) !== -1; } changePlayerColor(playerId, color) { this.room.setPlayerDiscProperties(playerId, { color, }); } addGoal(scorerName, teamId) { const scores = this.room.getScores(); this.goals.push({ goalScorerName: scorerName, goalSide: this.getTeamName(teamId), goalSpeed: this.ballSpeed, goalTime: scores.time, }); } pointDistance(p1, p2) { const d1 = p1.x - p2.x; const d2 = p1.y - p2.y; return Math.sqrt(d1 * d1 + d2 * d2); } clearPlayersOffsidePosition(teamId) { let offsidePlayers = {}; if (!teamId) { offsidePlayers = { ...this.playersOffsidePosition[RED_TEAM_ID], ...this.playersOffsidePosition[BLUE_TEAM_ID] }; this.playersOffsidePosition = { [RED_TEAM_ID]: {}, [BLUE_TEAM_ID]: {}, }; } else { offsidePlayers = { ...this.playersOffsidePosition[teamId] }; this.playersOffsidePosition[teamId] = {}; } this.clearPlayersAvatars(offsidePlayers); } clearPlayersAvatars(offsidePlayers) { const ids = Object.keys(offsidePlayers); for (let i = 0; i < ids.length; i++) { const playerId = ids[i]; const player = this.room.getPlayer(playerId); if (player) { this.resetPlayerAvatar(player); } } } clearTouchingTheBallTimestamp() { this.touchingTheBallTimestamps = {}; } clear() { this.touchingTheBallTimestamps = {}; this.ballSpeed = 0; this.prevBallPosition = null; this.goals = []; this.gameStartTimestamp = null; this.gameEndTimestamp = null; this.tick = 0; this.isPaused = false; this.votesForUnpause = {}; this.playersOffsidePosition = { [RED_TEAM_ID]: {}, [BLUE_TEAM_ID]: {}, }; } resetPlayerAvatar(player) { const avatar = playersAvatars[player.name] || player.id; this.room.setPlayerAvatar(player.id, `${avatar}`); } resetTeamToInitPosition(teamId) { const playerList = this.getPlayers(); for (let i = 0; i < playerList.length; i++) { const player = playerList[i]; if (!teamId || (teamId && player.team === teamId)) { const initPosition = this.playersInitPosition[player.team][player.id]; this.room.setPlayerDiscProperties(player.id, { x: initPosition.x, y: initPosition.y, xspeed: 0, yspeed: 0, }); } } } goToGameTab() { try { const roomLinkElement = this.getRoomLinkElement(); this.gamePageController = new GamePageController(roomLinkElement.href); } catch (error) { console.log(error); } } getRoomLinkElement() { return $(document.getElementsByTagName('iframe')[0].contentWindow.document.body).find('a')[1]; } waitForRoomLinkElement(callback) { const element = this.getRoomLinkElement(); if (element) { callback(); } else { setTimeout(() => { this.waitForRoomLinkElement(callback); }, 500); } }; addSaveReplayButton() { if (document.getElementById(SAVE_REPLAY_BUTTON_ID)) { return; } let button = document.createElement("button"); button.id = SAVE_REPLAY_BUTTON_ID; button.innerHTML = "SAVE REPLAY"; button.onclick = this.saveMatchResult.bind(this); button.style.color = '#fff'; button.style.height = '100px'; button.style.width = '450px'; button.style.position = 'fixed'; button.style.top = '300px'; button.style.left = '10px'; button.style.fontSize = '36px'; button.style.background = 'linear-gradient(#8da86b, #658d59)'; button.style.border = '4px solid white'; button.style.borderRadius = '50px'; button.style.cursor = 'pointer'; button = document.body.appendChild(button); } saveMatchResult() { try { if (this.matchResult) { this.client.postMatchResult(this.matchResult, this.handlePostMatchResult.bind(this)); } else { throw new Error('Match not found'); } } catch (error) { console.log(error); } } setHostHandicap() { this.gamePageController.sendMessage('/handicap ' + HOST_HANDICAP); } } class GamePageController { constructor(pageUrl) { this.page = window.open(pageUrl); } getDocument() { return this.page.document.getElementsByTagName('iframe')[0].contentWindow.document; } getInputBox() { return this.getDocument().getElementsByClassName('input')[0]; } getInput() { return this.getInputBox().children[0]; } getSendButton() { return this.getInputBox().children[1]; } sendMessage(message = '') { const input = this.getInput(); const button = this.getSendButton(); input.value = message; button.click(); } showConfirmModal(message = '') { return this.page.confirm(message); } } class Client { getCalculatedTeams(playerList, callback) { let url = BACKEND_BASE_URL + 'findTeams?' for (let i = 0; i < playerList.length; i++) { var player = playerList[i]; url += 'players[]=' + player.name + '&'; } $.get(url, function (data) { callback(data.red, data.blue); }); } postMatchResult(matchResult, callback){ $.ajax({ type: "POST", url: BACKEND_BASE_URL + 'calculatedMatch/new', // move this to const dataType: 'application/json', data: matchResult, success: callback, }); } } function init(){ try { console.log('--- starting room ---'); var haxBallController = new HaxBallController() .initRoom() .initListeners() .initUserInterface() ; window.haxBallController = haxBallController; window.room = haxBallController.getRoom(); console.log('--- room started ---'); } catch (error) { console.log('fooking error', error); } } (function() { 'use strict'; window.onHBLoaded = init; })();