From 48397199c55780b15dd187505f388422a52c82a4 Mon Sep 17 00:00:00 2001 From: Geoffrey Frogeye Date: Fri, 5 Jun 2015 16:22:41 +0200 Subject: [PATCH] Initial commit Note: Some balls don't render. This is mostly a prevent-lost-work commit. --- .gitignore | 1 + .gitmodules | 3 + Makefile | 3 + README.md | 13 ++ app.html | 21 +++ client | 1 + loader.js | 2 + script.js | 457 ++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 9 ++ 9 files changed, 510 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 README.md create mode 100644 app.html create mode 160000 client create mode 100644 loader.js create mode 100644 script.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e804e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bundle.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f0b8027 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "client"] + path = client + url = git@github.com:pulviscriptor/agario-client.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..abd95c0 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +bundle.js: loader.js client/agario-client.js + browserify $< -o /tmp/agario-webclient-makefile -i buffer-dataview -i ws + sed -i '/var WebSocket/d' $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..23be51e --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +#Web-browser client for [agar.io](http://agar.io) +A small browser-based graphical client for agar.io, written using [pixijs](http://www.pixijs.com/) and [agario-client](https://github.com/pulviscriptor/agario-client). + +**DISCLAIMER:** This project isn't affiliated with agar.io in any way. When playing with this client, you don't get ads, which is nice for you maybe, but it doesn't pay for the servers needed to run the game. + +**Note:** You won't be able to play directly on agar.io servers due to cross origin policy restrictions. You must use a proxy (you can for example use the repeater you can find in [agario-devtools](https://github.com/pulviscriptor/agario-devtools)). + +##Usage + +* Clone the repo with the `--recursive` option. If it was cloned without this option, run `git submodule update --init --recursive` +* Install [browserify](http://browserify.org/) globally +* Run `make` +* Open `app.html` from the browser diff --git a/app.html b/app.html new file mode 100644 index 0000000..8f306e1 --- /dev/null +++ b/app.html @@ -0,0 +1,21 @@ + + + + + + agario-webclient + + + + + + + + + + +
+
+ + + \ No newline at end of file diff --git a/client b/client new file mode 160000 index 0000000..c1a834d --- /dev/null +++ b/client @@ -0,0 +1 @@ +Subproject commit c1a834d8e66193eb6bc60951dc784ea9ca20906b diff --git a/loader.js b/loader.js new file mode 100644 index 0000000..2c2e7d3 --- /dev/null +++ b/loader.js @@ -0,0 +1,2 @@ +window.Client = require('./client/agario-client.js'); +window.EventEmitter = require('events').EventEmitter; diff --git a/script.js b/script.js new file mode 100644 index 0000000..7b6e90b --- /dev/null +++ b/script.js @@ -0,0 +1,457 @@ +function AnimatedValue(object, name, value, timeout) { + this.overwrite(value); + this.timeout = timeout; + var _this = this; + Object.defineProperty(object, name, { + get: function() { + return _this.get(); + }, + set: function(x) { + return _this.set(x); + } + }); + object[name + 'handler'] = this; +} + +AnimatedValue.prototype = { + get: function() { + var now = performance.now(), + end = this.frTime + this.timeout; + if (now >= end) { + return this.toVal; + } else { + return this.toVal - (this.toVal - this.frVal) * (end - now) / this.timeout; + } + }, + set: function(value) { + if (value != this.toVal) { + this.frVal = this.get(); + this.toVal = value; + this.frTime = performance.now(); + } + }, + overwrite: function(value) { + this.frVal = value; + this.toVal = value; + this.frTime = performance.now() - this.timeout; // so end == now + } +}; + +function BallView(main, ball) { + this.main = main; + this.ball = ball; + this.graphic = new PIXI.Graphics(); + + new AnimatedValue(this, 'x', 0, 100); + new AnimatedValue(this, 'y', 0, 100); + new AnimatedValue(this, 's', 0, 100); + + var _this = this; + // console.log(_this.ball.id, 'NEW'); + this.appear(); + this.ball.on('appear', function() { + _this.appear(); + }); + this.ball.on('destroy', function() { + // console.log(_this.ball.id, 'DES'); + _this.main.stage.removeChild(_this.graphic); + }); + this.ball.on('disappear', function() { + // console.log(_this.ball.id, 'DIS'); + _this.main.stage.removeChild(_this.graphic); + }); + this.ball.on('move', function() { + _this.x = _this.ball.x; + _this.y = _this.ball.y; + }); + this.ball.on('resize', function() { + _this.s = _this.ball.size; + }); +} + +BallView.prototype = { + appear: function() { + // console.log(this.ball.id, 'APP'); + this.x = this.ball.x; + this.y = this.ball.y; + this.s = this.ball.size; + this.shape(); + this.main.stage.addChild(this.graphic); + }, + shape: function() { + this.graphic.clear(); + this.graphic.beginFill(this.ball.color.replace('#', '0x'), + this.ball.virus ? 0.5 : 0.9); + this.graphic.drawCircle(0, 0, 1); + this.graphic.endFill(); + }, + render: function() { + this.graphic.position.x = this.x; + this.graphic.position.y = this.y; + this.graphic.scale.x = this.graphic.scale.y = this.s; + } +}; + + +function Viewer(client, container) { + this.client = client; + this.container = container; + + this.balls = {}; + + this.addRenderer(); + this.addStats(); + var _this = this; + client.once('mapSizeLoad', function(min_x, min_y, max_x, max_y) { + _this.gameWidth = max_x; + _this.gameHeight = max_y; + _this.initStage(); + _this.addListners(); + _this.addBorders(); + _this.animate(); + _this.emit('launched'); + }); + window.addEventListener('resize', function() { + _this.updateSize(); + }); +} + +Viewer.prototype = { + getSize: function() { + this.width = window.innerWidth; + this.height = window.innerHeight; + }, + addRenderer: function() { + this.getSize(); + this.renderer = PIXI.autoDetectRenderer(this.width, this.height, { + antialias: true + }); + this.container.appendChild(this.renderer.view); + }, + updateSize: function() { + this.getSize(); + this.renderer.resize(this.width, this.height); + }, + initStage: function() { + this.stage = new PIXI.Container(); + this.cam = {}; + new AnimatedValue(this.cam, 'x', this.gameWidth / 2, 100); + new AnimatedValue(this.cam, 'y', this.gameHeight / 2, 100); + new AnimatedValue(this.cam, 's', 0.5 / 2, 100); + }, + addListners: function() { + var _this = this; + this.client.on('ballAppear', function(id) { + if (!_this.balls[id]) { + _this.balls[id] = new BallView(_this, this.balls[id]); + } + }); + this.client.on('ballDestroy', function(id) { + delete this.balls[id]; + }); + }, + addBorders: function() { + this.borders = new PIXI.Graphics(); + this.borders.lineStyle(5, 0xFF3300, 1); + this.borders.drawRect(0, 0, this.gameWidth, this.gameHeight); + this.stage.addChild(this.borders); + }, + addStats: function() { + this.stats = new Stats(); + this.stats.setMode(1); + this.stats.domElement.style.position = 'absolute'; + this.stats.domElement.style.left = '0px'; + this.stats.domElement.style.top = '0px'; + document.body.appendChild(this.stats.domElement); + }, + posCamera: function() { + var x = y = p = 0; + for (var ball_id in this.client.my_balls) { + var ball = this.client.balls[this.client.my_balls[ball_id]]; + if (!ball.visible) continue; + x += ball.x * ball.size; + y += ball.y * ball.size; + p += ball.size; + } + if (p > 0) { // if we have visible ball(s) + this.cam.x = x / p; + this.cam.y = y / p; + this.cam.s = 0.04 * this.width / p; // Scale + // TODO Better scale calculation + } // else: don't move the camera + }, + render: function() { + for (var ball_id in this.client.balls) { + var ball = this.balls[ball_id]; + if (ball) { + ball.render(); + } + } + }, + animate: function() { + this.stats.begin(); + this.render(); + this.posCamera(); + this.stage.scale.x = this.stage.scale.y = this.cam.s; + this.stage.position.x = -this.cam.x * this.stage.scale.x + this.width / 2; + this.stage.position.y = -this.cam.y * this.stage.scale.y + this.height / 2; + this.renderer.render(this.stage); + this.stats.end(); + this.emit('animate'); + var _this = this; + requestAnimationFrame(function() { + _this.animate(); + }); + } +}; + +// Inherit from EventEmitter +for (var key in EventEmitter.prototype) { + Viewer.prototype[key] = EventEmitter.prototype[key]; +} + +function Pointer(viewer) { + this.viewer = viewer; + this.client = this.viewer.client; + this.dest = { // Destination, relative to camera center + x: 0, + y: 0 + }; + var _this = this; + this.viewer.once('launched', function() { + _this.viewer.stage.interactive = true; + _this.viewer.stage.on('mousemove', function(e) { + _this.pointermove(e); + }); + _this.viewer.stage.on('touchmove', function(e) { + _this.pointermove(e); + }); + _this.viewer.on('animate', function(e) { + _this.move(); + }); + }); + window.addEventListener('keydown', function(e) { + if (e.keyCode == 87) { + _this.client.eject(); + } else if (e.keyCode == 32) { + _this.client.split(); + } + }); +} + +Pointer.prototype = { + move: function() { + this.client.moveTo(this.viewer.cam.x + this.dest.x, this.viewer.cam.y + this.dest.y); + }, + pointermove: function(e) { + var gamePos = e.data.getLocalPosition(this.viewer.stage); + this.dest = { // TODO deadzone + x: gamePos.x - this.viewer.cam.x, + y: gamePos.y - this.viewer.cam.y + }; + this.move(); + } +}; + +function Controller(client) { + this.client = client; + + this.server = { + region: 'EU-London', + ip: '127.0.0.1', + port: 9158 + } + this.nick = 'agario-client'; + this.autoRespawn = false; + + this.gui = new dat.GUI(); + + this.servgui = this.gui.addFolder('Server'); + this.servgui.add(this.server, 'region', ['US-Fremont', 'US-Atlanta', 'BR-Brazil', 'EU-London', 'RU-Russia', 'JP-Tokyo', 'CN-China', 'SG-Singapore', 'TK-Turkey']); + this.servgui.add(this, 'findServer'); + this.servgui.add(this.server, 'ip'); + this.servgui.add(this.server, 'port'); + this.servgui.add(this, 'connect'); + this.servgui.add(this, 'disconnect'); + this.servgui.open(); + + this.cellgui = this.gui.addFolder('Cell'); + this.cellgui.add(this, 'nick'); + this.cellgui.add(this, 'spawn'); + this.cellgui.add(this, 'autoRespawn'); + + this.leadergui = this.gui.addFolder('Leaderboard'); + this.leaders = {}; + this.resetLeader(); + for (var i = 1; i <= 10; i++) { + this.leadergui.add(this.leaders, i); + } + + var _this = this; + client.on('connected', function() { + _this.servgui.close(); + _this.cellgui.open(); + _this.leadergui.open(); + if (_this.autoRespawn) { + _this.spawn(); + } + }); + client.on('reset', function() { + _this.servgui.open(); + _this.cellgui.close(); + _this.leadergui.close(); + _this.resetLeader(); + }); + client.on('lostMyBalls', function() { + if (_this.autoRespawn) { + _this.spawn(); + } + }); + client.on('leaderBoardUpdate', function(old, leaders) { + for (var i in leaders) { + var rank = parseInt(i) + 1; + _this.leaders[rank] = this.balls[leaders[i]].name || 'An unnamed cell'; + for (var j in _this.leadergui.__controllers) { + _this.leadergui.__controllers[j].updateDisplay(); + } + } + }); +} + +Controller.prototype = { + findServer: function() { + // Because of SOP, this will never work + x = new XMLHttpRequest(); + x.open('POST', 'http://m.agar.io', false); + x.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + x.setRequestHeader('Content-Length', this.server.region.length); + // x.setRequestHeader('Origin', 'http://agar.io'); + // x.setRequestHeader('Referer', 'http://agar.io/'); + x.send(this.server.region); + s = x.responseText.split(':'); + this.server.ip = s[0]; + this.server.port = s[1]; + + for (var i in this.servgui.__controllers) { + this.servgui.__controllers[i].updateDisplay(); + } + }, + connect: function() { + this.client.connect('ws://' + this.server.ip + ':' + this.server.port); + }, + disconnect: function() { + this.client.disconnect(); + }, + spawn: function() { + this.client.spawn(this.nick); + }, + resetLeader: function() { + for (var i = 1; i <= 10; i++) { + this.leaders[i] = '---'; + } + } +}; + +function IA(client) { + this.client = client; + this.begin(); +} + +IA.prototype = { + begin: function() { + var _this = this; + this.interval = setInterval(function() { + _this.food(); + // _this.decide(); + }, 100); + }, + end: function() { + clearInterval(this.interval_id); + }, + getDistanceBetweenBalls: function(ball_1, ball_2) { //this calculates distance between 2 balls + return Math.sqrt(Math.pow(ball_1.x - ball_2.x, 2) + Math.pow(ball_2.y - ball_1.y, 2)); + }, + getAngleBetweenBalls: function(b1, b2) { // output in rad + dX = b2.x - b1.x; + dY = b2.y - b1.y; + return Math.tan(dY / dX); + }, + food: function() { + var candidate_ball = null; //first we don't have candidate to eat + var candidate_distance = 0; + var my_ball = this.client.balls[this.client.my_balls[0]]; //we get our first ball. We don't care if there more then one, its just example. + if (!my_ball) return; //if our ball not spawned yet then we abort. We will come back here in 100ms later + + for (var ball_id in this.client.balls) { //we go true all balls we know about + var ball = this.client.balls[ball_id]; + if (ball.virus) continue; //if ball is a virus (green non edible thing) then we skip it + if (!ball.visible) continue; //if ball is not on our screen (field of view) then we skip it + if (ball.mine) continue; //if ball is our ball - then we skip it + if (ball.size / my_ball.size > 0.5) continue; //if ball is bigger than 50% of our size - then we skip it + var distance = this.getDistanceBetweenBalls(ball, my_ball); //we calculate distances between our ball and candidate + if (candidate_ball && distance > candidate_distance) continue; //if we do have some candidate and distance to it smaller, than distance to this ball, we skip it + + candidate_ball = ball; //we found new candidate and we record him + candidate_distance = this.getDistanceBetweenBalls(ball, my_ball); //we record distance to him to compare it with other balls + } + if (!candidate_ball) return; //if we didn't find any candidate, we abort. We will come back here in 100ms later + + this.client.log('closest ' + candidate_ball + ', distance ' + candidate_distance); + this.client.moveTo(candidate_ball.x, candidate_ball.y); //we send move command to move to food's coordinates + }, + decide: function() { + var my_ball = this.client.balls[this.client.my_balls[0]]; // TODO Handle more balls + if (!my_ball) return; + var candidates = []; + + for (var ball_id in this.client.balls) { + var ball = this.client.balls[ball_id]; + if (!ball.visible) continue; + + var score = 0; + if (ball.mine) { + score = 1; + } else { + if (ball.virus) { + if (ball.size > my_ball.size) { + score = -5; + } else { + score = 5; + } + } else { + score = Math.max(1000 - this.getDistanceBetweenBalls(ball, my_ball) - ball.size - my_ball.size, 0); + if (ball.size < my_ball.size) { + score = score; + } else { + score = -score * 5; + } + } + } + + candidates.push({ + x: ball.x - my_ball.x, + y: ball.y - my_ball.y, + score: score + }); + } + + var x = y = p = 0; + for (var candidate_id in candidates) { + var candidate = candidates[candidate_id]; + // console.log(candidate.x, candidate.y, candidate.score) + x += candidate.x * candidate.score; + y += candidate.y * candidate.score; + p += candidate.score; + } + this.client.moveTo(my_ball.x + x / p, my_ball.y + y / p); + } +}; + +var d = {}; // DEBUG Allow access from console +window.onload = function() { + d.client = new Client('worker'); + d.viewer = new Viewer(d.client, document.getElementById('viewer')); + d.controller = new Controller(d.client); + d.pointer = new Pointer(d.viewer); + // d.ia = new IA(d.client); +}; diff --git a/style.css b/style.css new file mode 100644 index 0000000..7446dba --- /dev/null +++ b/style.css @@ -0,0 +1,9 @@ +body, +#viewer, +canvas { + margin: 0; +} + +body { + overflow: hidden; +}