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;
})();