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