// ==UserScript==
// @name Truffle Pig Public
// @namespace Truffle Pig Public
// @version 0.9.1
// @description Finding all the tasty truffles for you! (Public Edition)
// @author Arimas
// @match https://agma.io/
// @icon https://www.google.com/s2/favicons?domain=agma.io
// @grant none
// ==/UserScript==
(function() {
'use strict';
var trufflePigPublic = {
// Mass := cell-area * factor
MASS_AREA_FACTOR: 0.0031828408,
// How many times must a cell be bigger to eat another cell?
CELL_EATING_FACTOR: 1.3, // TODO: Check!
// Amount of mass at spawn. ATTENTION: The mass can be either this or less than this!
MAX_START_SPAWN_MASS: 141,
// How big must a cell be to pick up a coin? 126-134???
COIN_PICKUP_MASS: 126,
// If true, bot will automatically respawn when dead
autoRespawn: true,
// If true, mouse movements by the user will override the movement of the bot
allowUserOverride: true,
// Last time (in milliseconds) when the user controlled the bot ( = moved the mouse)
userControlledAt: null,
// If set to true, show debugging information. Do not change this after init!
debug: false,
// Int - current player position as it is used by the camera (midpoint of all my cells)
x: null,
// Int - current player position as it is used by the camera (midpoint of all my cells)
y: null,
// Is unknown so will be figured out while playing (prob main room borders could be detected though)
mapWidth: 0,
mapHeight: 0,
// Maximum number of cells (pieces) a player can have (we dont really know that due to diff. servers so have to learn it on the fly)
maxCells: 64,
// My cells - with x, y, mass, radius, area. NOTE: The smallest cell has index 0, the biggest has the highest index!
cells: [],
// My cells, but only temporary. Will be copied into the cells array once its filled
tempCells: [],
// Visible coins
coins: [],
// Coinbs, but only tewmporarytemporary. Will be copied into the coins array once its filled.
tempCoins: [],
// Int - current total player mass (incorrect when the player is in portals!)
mass: 0,
// Int - temporary current total player mass, will be copied to the mass property
tempMass: 0,
// Is the bot currently alive?
alive: false,
// URL of my skin
skinUrl: null,
// Zoom factor
zoom: null,
// If true bot doesnt do anything
stopped: false,
// Time when the bot was initialized
startedAt: null,
// Last time when the bot spawned
spawnedAt: null,
// Last time (in milliseconds) when we tried to split
splitAt: null,
// Counter for the main loop iterations
iteration: 0,
// Is the bot respawning right now? (This is a process that needs several seconds to complete)
respawning: false,
// Saves the official key bindings
hotkeys: null,
startCoins: 0,
// Original drawImage() function
originalDrawImage: null,
/**
* Start the bot
*/
init: function() {
var self = this;
window.ventron = this;
this.startedAt = new Date();
this.skinUrl = this.getSkinUrl();
if (this.skinUrl == 'https://agma.io/skins/0_lo.png') {
alert('No skin chosen - bot does not work. Pick skin and reload page.');
return;
}
if (this.debug) {
var $crosshair = $('<div id="bot-crosshair" style="position: fixed; left: 50%; top: 50%; width: 4px; height: 4px; margin-left: -2px; margin-top: -2px; background-color: red; z-index: 999"></div>');
$('body').append($crosshair);
}
setFixedZoom(true);
var agmaSettings = JSON.parse(localStorage.getItem('settings'));
if (agmaSettings.fixedZoomScale > 0.4) {
alert('Please zoom out a bit.');
}
this.startCoins = this.getCoins();
this.originalDrawImage = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = this.drawImage;
$(document).mousemove(function(event) {
// Synthetic events are those we create when moving the virtual mouse pointer
if (! event.synthetic && self.allowUserOverride) {
self.userControlledAt = Date.now();
}
});
// If the shop close button is clicked, get and save my skin URL - it may have been changed
$('#shopModalDialog .close').click(function() {
setTimeout(function() {
self.skinUrl = self.getSkinUrl();
}, 100);
});
$('#chtbox').keydown(function(event) {
if (event.keyCode == 13) {
if (self.checkChatBox()) {
$('#chtbox').val('');
}
}
});
this.hotkeys = JSON.parse(localStorage.getItem('hotkeys'));
window.addEventListener('keypress', function(event)
{
// Do nothing if a menu is open
if (document.getElementById('overlays').style.display !== 'none' || document.getElementById('advert').style.display !== 'none') {
return;
}
// Ignore text input fields
if (document.activeElement.type === 'text' || document.activeElement.type === 'password') {
return;
}
});
if (this.debug) {
this.$debugOutput = $('<div style="position: fixed; left: 250px; top: 75px; z-index: 9999; color: #3e3e3e; pointer-events: none">');
$('body').append(this.$debugOutput);
}
var originalRequest = window.requestAnimationFrame;
window.requestAnimationFrame = function (callback) {
var result = originalRequest.apply(this, arguments);
self.mass = self.tempMass;
self.cells = self.tempCells;
self.coins = self.tempCoins;
self.run.apply(self);
self.tempMass = 0;
self.tempCells = [];
self.tempCoins = [];
return result;
};
originalRequest(this.run.bind(this));
let message = '🐷 Truffle Pig Public is ready! The truffles will be yours.';
self.swal(
'Truffle Pig Public Bot',
message + '<br><br><b>ATTENTION</b>: Bot needs a skin to be put on.<br> Bot works best on Solo AGF.<br> Type <i>/bot help</i> for help!<br> Bot cant split while you type in the chat!');
console.log('%' + message, 'background-color: black; color: pink; font-weight: bold; padding:5px;');
},
/**
* Main method. Once started, it is running in a never ending loop.
*/
run: function() {
var agmaSettings = JSON.parse(localStorage.getItem('settings'));
this.zoom = agmaSettings.fixedZoomScale;
if (this.stopped) {
return;
}
if (this.isDeathPopupVisible()) {
this.alive = false;
}
if (this.autoRespawn && ! this.alive) {
this.respawn();
}
if (this.alive) {
if (this.mass > 0 && this.mass < 0.75 * this.MAX_START_SPAWN_MASS && ! this.respawning &&
(this.coins.length == 0 || this.mass < this.COIN_PICKUP_MASS)) {
this.respawn(); // No return - respawn does not always work!!
}
if (this.spawnedAt !== null && Date.now() - this.spawnedAt > 30000 && this.coins.length == 0) {
this.respawn();
}
if (this.spawnedAt !== null && Date.now() - this.spawnedAt > 120000) {
this.respawn();
}
if (! this.collectCoin()) {
if (this.cells.length < 16) {
self.macroSplit();
}
let angle = null;
if (Date.now() - this.startedAt > 3 * 60 * 1000 && Date.now() - this.changedTargetAt > 1000) {
if (this.x < 0.03 * this.mapWidth) {
this.angle = this.getRandomInt(0 + 30,180 - 30);
}
if (this.x > 0.97 * this.mapWidth) {
this.angle = this.getRandomInt(180 + 30,359 - 30);
}
if (this.y < 0.03 * this.mapHeight) {
this.angle = this.getRandomInt(90 + 30,270 - 30);
}
if (this.y > 0.97 * this.mapHeight) {
this.angle = this.getRandomInt(270 + 30, 360 + 90 - 30) % 360;
}
}
if (this.changedTargetAt === undefined || Date.now() - this.changedTargetAt > 3000 ||angle !== null) {
if (Date.now() - this.startedAt > 3 * 60 * 1000) {
if (this.x < 0.1 * this.mapWidth) {
this.angle = this.getRandomInt(0 + 30,180 - 30);
}
if (this.x > 0.9 * this.mapWidth) {
this.angle = this.getRandomInt(180 + 30,359 - 30);
}
if (this.y < 0.1 * this.mapHeight) {
this.angle = this.getRandomInt(90 + 30,270 - 30);
}
if (this.y > 0.9 * this.mapHeight) {
this.angle = this.getRandomInt(270 + 30, 360 + 90 - 30) % 360;
}
}
if (angle === null) {
angle = this.getRandomInt(1,359);
}
this.steerAngle(angle);
this.changedTargetAt = Date.now();
}
}
}
this.iteration++;
},
/**
* This overwrites the original drawImage function. This allows us to get all the drawImage() calls,
* with coordinates of the images, so we can get the position of things on the map, for instance of cells.
*/
drawImage: function (image, sourceX, sourceY, sourceWidth, sourceHeight, targetX, targetY, targetWidth, targetHeight) {
var self = window.ventron;
var radius, mass;
if (this.canvas.id === 'canvas') {
// Detect myself (one of my cells)
if (self.skinUrl && (image.src == self.skinUrl.replace('_lo.', '.') || image.src == self.skinUrl)) {
self.alive = true;
if (self.spawnedAt === null) {
self.spawnedAt = Date.now();
}
radius = sourceWidth / 2;
mass = self.getCellMass(radius);
var x = parseInt(sourceX + radius);
var y = parseInt(sourceY + radius);
if (x > self.mapWidth) {
self.mapWidth = x;
}
if (y > self.mapHeight) {
self.mapHeight = y;
}
self.tempMass += mass;
self.cloaked = (this.globalAlpha < 1);
self.tempCells.push({ x: x, y: y, mass: mass, radius: radius });
self.x = 0;
self.y = 0;
self.tempCells.forEach(function(cell) {
self.x += cell.x;
self.y += cell.y;
});
self.x /= self.tempCells.length;
self.y /= self.tempCells.length;
if (self.tempCells.length > self.maxCells) {
self.maxCells = self.tempCells.length
}
if (self.debug) {
//var $crosshair = $("#bot-crosshair");
//$("#bot-crosshair").css('left', self.getScreenPosX(sourceX));
//$("#bot-crosshair").css('top', self.getScreenPosY(sourceY));
self.$debugOutput.text('x: ' + parseInt(self.x) + ', y: ' + parseInt(self.y) + ', mass: ' + parseInt(self.tempMass) + ', cells: ' + self.tempCells.length);
}
}
// Detect coin
if ((image.src == 'https://agma.io/skins/objects/9_lo.png?v=1' || image.src == 'https://agma.io/skins/objects/9.png?v=1')) {
let matrix = this.getTransform() ;
self.tempCoins.push({x: self.getGamePosX(matrix.e), y: self.getGamePosY(matrix.f)});
}
}
return self.originalDrawImage.apply(this, arguments);
},
collectCoin: function() {
// Eat coins
if (this.coins.length > 0 && this.mass > this.COIN_PICKUP_MASS) {
// Find the closest coin
let coin = null, minDistance = Number.MAX_VALUE;
this.coins.forEach(function(inspectedCoin) {
let distance = self.getDistance(self.x, self.y, inspectedCoin.x, inspectedCoin.y);
if (distance < minDistance) {
minDistance = distance;
coin = inspectedCoin;
}
});
if (minDistance < 100 && this.cells.length > 1) {
self.merge();
} else {
let myScreenX = this.getScreenPosX(this.x), myScreenY = this.getScreenPosY(this.y);
let screenDistance = self.getDistance(myScreenX, myScreenY, this.getScreenPosX(coin.x), this.getScreenPosX(coin.y));
self.steer(coin.x, coin.y, screenDistance + 50);
}
if (this.coinsMode && Date.now() - this.splitAt > 500) {
if (this.cells.length === 1 && this.mass > 2 * this.COIN_PICKUP_MASS && minDistance > 50) {
self.split();
} else {
// if (this.cells.length === 1 && this.mass > 4 * this.COIN_PICKUP_MASS && minDistance > 100) {
// self.doubleSplit();
//}
}
}
return true;
}
return false;
},
merge: function(targetX, targetY)
{
var mouseX, mouseY;
if (targetX === undefined || targetY === undefined) {
mouseX = window.innerWidth / 2 + this.getRandomInt(-15, 15);
mouseY = window.innerHeight / 2 + this.getRandomInt(-15, 15);
} else {
mouseX = this.getScreenPosX(targetX);
mouseY = this.getScreenPosY(targetY);
}
$('canvas').trigger($.Event('mousemove', {clientX: mouseX, clientY: mouseY, synthetic: true}));
},
/**
* Moves the bot directly in the direction of given coordinates.
* Won't avoid obstacles, won't use pathfinding.
* It does not matter of the coordinates are on the map or not.
*/
steer: function(targetX, targetY, screenDistance, sourceX, sourceY) {
if (typeof screenDistance === 'undefined') {
screenDistance = Math.ceil(Math.max(window.innerWidth, window.innerHeight) / 2);
}
if (sourceX === undefined || sourceY === undefined) {
sourceX = this.x;
sourceY = this.y;
}
var angle = this.getAngle(sourceX, sourceY, targetX, targetY);
this.steerAngle(angle, screenDistance);
},
/**
* Moves the bot directly in the direction of a given angle.
* Won't avoid obstacles, won't use pathfinding.
* It does not matter of the coordinates are on the map or not.
*/
steerAngle: function(angle, screenDistance) {
if (typeof screenDistance === 'undefined') {
screenDistance = Math.ceil(Math.max(window.innerWidth, window.innerHeight) / 2);
}
var mouseX = window.innerWidth / 2 + Math.sin(angle * Math.PI / 180) * screenDistance;
var mouseY = window.innerHeight / 2 - Math.cos(angle * Math.PI / 180) * screenDistance;
$('canvas').trigger($.Event('mousemove', {clientX: mouseX, clientY: mouseY, synthetic: true}));
},
/**
* Moves the bot directly in the direction of given game world coordinates,
* by moving the (virtual) mouse pointer to that position on the screen.
* Won't avoid obstacles, won't use pathfinding.
* It does not matter of the coordinates are on the map or not.
*/
steerToGamePos: function(gameTargetX, gameTargetY) {
var mouseX = this.getScreenPosX(gameTargetX);
var mouseY = this.getScreenPosY(gameTargetY);
$('canvas').trigger($.Event('mousemove', {clientX: mouseX, clientY: mouseY, synthetic: true}));
},
/**
* Tries to make the bot split by sending the splitting key.
* Only works if the the bot is alive, has enough mass, 2+ pieces and not too many pieces.
* ATTENTION: Split does not happen immediately but with a short delay!
*/
split: function() {
$('body').trigger($.Event('keydown', { keyCode: this.hotkeys.Space.c}));
$('body').trigger($.Event('keyup', { keyCode: this.hotkeys.Space.c}));
this.splitAt = Date.now();
},
/**
* Tries to make the bot double split by sending the double split key.
* Only works if the the bot is alive, has enough mass, 2+ pieces and not too many pieces.
*/
doubleSplit: function() {
$('body').trigger($.Event('keydown', { keyCode: this.hotkeys.D.c}));
$('body').trigger($.Event('keyup', { keyCode: this.hotkeys.D.c}));
this.splitAt = Date.now();
},
/**
* Tries to make the bot triple split by sending the triple split key.
* Only works if the the bot is alive, has enough mass, 2+ pieces and not too many pieces.
*/
tripleSplit: function() {
$('body').trigger($.Event('keydown', { keyCode: this.hotkeys.T.c}));
$('body').trigger($.Event('keyup', { keyCode: this.hotkeys.T.c}));
this.splitAt = Date.now();
},
/**
* Tries to make the bot macro split (16 split) by sending the macro split key.
* Only works if the the bot is alive, has enough mass, 2+ pieces and not too many pieces.
*/
macroSplit: function() {
$('body').trigger($.Event('keydown', { keyCode: this.hotkeys.Z.c}));
$('body').trigger($.Event('keyup', { keyCode: this.hotkeys.Z.c}));
this.splitAt = Date.now();
},
/**
* Respawns the bot/player. Uses the respawn key if the bot is alive, uses the menu otherwise.
*/
respawn: function() {
self = this;
if (self.respawning) {
return;
}
self.respawning = true;
if (this.spawnedAt !== null) {
this.spawnedAt = null; // Will be set in the draw method
}
this.splitAt = null;
this.ejectedAt = null;
if (self.alive) {
window.onkeydown({keyCode: this.hotkeys.M.c});
window.onkeyup({keyCode: this.hotkeys.M.c});
self.respawning = false;
} else {
if (self.isDeathPopupVisible()) {
// The ad cannot be closed immediately
setTimeout(function() {
closeAdvert();
self.spawn();
}, 2800);
} else {
self.spawn();
}
}
},
/**
* Spawns the bot by using the menu. This does not work if the bot is alive,
* use respawn() in that case!
*/
spawn: function() {
self = this;
var performSpawn = function() {
setNick(document.getElementById('nick').value);
self.spawnedAt = null; // Will be set in the draw method
// Respawning doesn't happen immediately, so wait a little bit
setTimeout(function() {
self.respawning = false;
}, 500);
};
// Spawning is not possible immediately so we check if we have to wait
if ($('#playBtn').css('opacity') < 1) {
setTimeout(function() {
performSpawn();
}, 2400);
} else {
performSpawn();
}
},
/**
* Displays a message at the top of the browser window, for a couple of seconds
*/
message: function(message) {
var curser = document.querySelector('#curser');
curser.textContent = message;
curser.style.display = 'block';
window.setTimeout(function() {
curser.style.display = 'none';
}, 5000);
},
/**
* Show a sweet alert (modal/popup) with a given title and message.
*/
swal: function (title, message, html) {
if (html === undefined) {
html = true;
}
window.swal({
title: '📢 <span class="miracle-primary-color-font">' + title + '</span>',
text: message,
html: html
});
},
/**
* Checks if there is a bot command in the chat bot (text input field).
* If that is the case, tries to execute the command.
* Note: The command won't be sent as a chat message.
*/
checkChatBox: function() {
var self = this;
var text = $('#chtbox').val();
if (text.substr(0, 5) == '/bot ') {
var command = text.substr(5);
// Function context = window.ventron
var execCommand = function() {
switch (command) {
case 'start':
this.stopped = false;
this.startCoins = 0;
this.startedAt = new Date();
this.message('Bot started!');
break;
case 'stop':
this.stopped = true;
this.message('Bot stopped!');
break;
case 'coins':
let coins = (this.getCoins() - this.startCoins);
let avg = Math.round(coins / ((Date.now() - this.startedAt) / 1000 / 60));
this.swal(coins + ' coins collected! ' + avg + ' per minute, ' + (avg * 60) + ' per hour.');
break;
case 'info':
this.stopped = false;
this.swal('Map width: ' + self.mapWidth + ', map height: ' + self.mapHeight);
break;
case 'help':
default:
this.swal('These commands are available: start, stop, coins, info');
}
}
setTimeout(execCommand.bind(self), 1);
return true;
}
},
getCoins: function()
{
return 1 * $('#coinsTopLeft').text().replace(/\s/g, '');
},
/**
* Calculates the (float) mass of a cell by its radius
* ATTENTION: It's important that this function returns a float!
* If the bot is split and small, the mass will be 0 else!
*/
getCellMass: function(radius) {
var area = Math.PI * radius * radius;
var mass = area * this.MASS_AREA_FACTOR;
return mass;
},
/**
* Calculates the radius of a cell by its mass
*/
getCellRadius: function(mass) {
var area = mass / this.MASS_AREA_FACTOR;
var radius = Math.sqrt(area / Math.PI);
return radius;
},
/**
* Returns the (float) distance between two points (coordinates)
*/
getDistance: function(x1, y1, x2, y2) {
return Math.hypot(x1 - x2, y1 - y2);
},
/**
* Returns the 360-angle between two points 8coordinates), starting by the first point.
* 0° means the second point is in the north of the first point.
* The returned angle will always be >= 0 and < 360.
*/
getAngle: function(x1, y1, x2, y2) {
var angle = Math.atan2(y2 - y1, x2 - x1); // range (-PI, PI]
angle *= 180 / Math.PI; // rads to degs, range (-180, 180]
angle += 90;
if (angle < 0) angle = 360 + angle; // range [0, 360)
if (angle >= 360) angle = 360 - angle; // range [0, 360)
return angle;
},
/**
* Adds angle2 to angle1 and returns the resulting angle.
*/
addAngle: function(angle1, angle2) {
var angle = angle1 + angle2;
angle %= 360;
if (angle < 0) angle += 360;
return angle;
},
/**
* Returns the (absolute) difference between two angles.
* The minimum difference will be 0, the maximum difference will be 180.
*/
getAngleDiff: function(angle1, angle2) {
var diff = angle1 - angle2;
diff = Math.abs(diff);
if (diff > 180) {
diff = 360 - diff;
}
return diff;
},
/**
* Transforms and returns an x coordinate on the screen to an x coordinate in the game.
*/
getGamePosX: function(screenPosX) {
return this.x - (window.innerWidth / 2 - screenPosX) / this.zoom;
},
/**
* Transforms and returns an y coordinate on the screen to an y coordinate in the game.
*/
getGamePosY: function(screenPosY) {
return this.y - (window.innerHeight / 2 - screenPosY) / this.zoom;
},
/**
* Transforms and returns an x coordinate in the game to an x coordinate on the screen.
*/
getScreenPosX: function(gamePosX) {
return - (- this.zoom * (gamePosX - this.x) - window.innerWidth / 2)
},
/**
* Transforms and returns an y coordinate in the game to an y coordinate on the screen.
*/
getScreenPosY: function(gamePosY) {
return - (- this.zoom * (gamePosY - this.y) - window.innerHeight / 2)
},
/**
* Returns a random integer between min (inclusive) and max (exclusive)
* Source: MDN
*/
getRandomInt: function(min, max) {
return parseInt(Math.random() * (max - min) + min);
},
/**
* Returns true if the popup that is displayed after we die is currently visible
*/
isDeathPopupVisible: function() {
var displayingAd = (document.getElementById('advert').style.display == 'block');
return displayingAd;
},
/**
* Returns the time since the bot was initialized in seconds
*/
getTimeSinceStart: function() {
var endDate = new Date();
return (endDate.getTime() - startDate.getTime()) / 1000;
},
/**
* Returns the URI of my skin or null if not skin has been set.
* Use this.skinUrl() to get it.
*/
getSkinUrl: function() {
var skinUrlRaw = $('#skinExampleMenu').css('background-image');
var parts = skinUrlRaw.split('"');
if (parts.length != 3) {
return null;
} else {
return parts[1];
}
},
}
let start = function() {
if (document.readyState === "complete") {
// We need to have a delay, because the skin preview in the game menu is not loaded right away and also we have to wait for auto login
setTimeout(function() {
trufflePigPublic.init();
}, 4000);
} else {
setTimeout(start, 1000);
}
};
start();
})();