diff --git a/.gitignore b/.gitignore index 9e610f2ce..99862063f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,12 @@ src/.settings/org.eclipse.wst.jsdt.ui.superType.name # Linux start script warning file src/.readwarning + +# Prevent possible banned IP address leak +src/ipbanlist.txt + +# Prevent user settings overwrite +src/gameserver.ini + +# Prevent certificate leak +ssl/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de613bdbc..64d6d48b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Contributions are appreciated in the form of pull requests. However, to maintain ... } ``` -* Unix-style line endings should be used (`\n`). +* CR-LF line endings should be used (`\r\n`). * Please leave a blank line at the end of each file. * Conditional/loop statements (`if`, `for`, `while`, etc.) should always use braces, and the opening brace should be placed on the same line as the statement. * There should be a space after a conditional/loop statement and before the condition, as well as a space after the condition and before the brace. Example: diff --git a/LICENSE.md b/LICENSE.md index 80864c5c3..6c1686b64 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,13 +1,231 @@ -Copyright 2015 Devin Ryan - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this project except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + Devin Ryan (https://github.com/forairan) saught out to have his license + listed. So this license can be found at the following location. + + Copyright 2015 Devin Ryan + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this project except in compliance with the License. + + See https://github.com/OgarProject/Ogar/blob/master/LICENSE.md + +-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + * INTERSTELLAR POLICE WARNING * + + As provided by the Galactic Treaty of 4410, this computer software product is + hereby declared the Interllectual Property of the human author, Barbosik. + All rights are henceforth reserved in space and time. + + Provision for the protection of Interlllectual Property is covered under + Section 8.9.1A-f of the Intengible Property Act of 4506, ratified by all + beings except the Gazurtoids of planet Gazuria. + + WARNING: Any being caught with an unautorized copy or version of this software + product will be punished by Interstellar Corporate Police. Punishment may + include the annihilation of the offending beings planet. + +-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/README.md b/README.md index 1968ae18d..fb2b24957 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,174 @@ -# Ogar -A fully functional open source Agar.io server implementation, written in Node.js. Ogar is designed to be used with the latest Agar.io client. - -### Official Website -The official website for the Ogar Project is [ogarproject.com](http://ogarproject.com). You can register on our forums to chat with other Ogar users, get support, advertise your server, and more. - -### Purchased Ogar? -If you've purchased a copy of Ogar, you've probably been ripped off. [This post on our website explains why.](http://ogarproject.com/threads/psa-if-you-purchased-ogar-youve-been-ripped-off.6/) - -## Obtaining and Using -If you are on Windows, you can download the latest binary build of Ogar [from this page](http://dl.ogarproject.com/). The binary is the easiest way to get started running an Ogar server. If you'd like to tinker with the source code, you can follow the instructions below (and slightly modify them) to run the source on Windows. - -As Ogar is written in Node.js, you must have Node.js, its "ws" and "vector2-node" modules installed to use it (unless you are using the Windows binary). You can usually download Node using your distribution's package manager (for *nix-like systems), or from [the Node website](http://nodejs.org). To install the "ws" module that is required, you can: -- for Windows, run `Install Dependecies.bat` located in the folder where this file is. -- for Mac, open your terminal, go to this directory with `cd` and type in `npm install`. -- for Linux, you can use the install script which would also automatically install node.js and ws. - -Manual: -```sh -~$ git clone git://github.com/OgarProject/Ogar.git Ogar -~$ npm install -~$ node Ogar -``` -Using the install script: -```sh -~$ sudo ogar-linux-script.sh install /your/preferred/directory -~$ sudo -u ogar -H /bin/sh -c "cd; /bin/node src/index.js" -``` -Using ```sudo -u ogar -H /bin/sh -c "cd; /bin/node src/index.js" ``` to launch the server increases security by running the process as an unprivileged, dedicated user with a limited shell and it is recommended to do so. - -Currently, Ogar listens on the following addresses and ports: -* *:80 - for the master server -* *:443 - for the game server - -Please note that on some systems, you may have to run the process as root or otherwise elevate your privileges to allow the process to listen on the needed ports. **If you are getting an EADDRINUSE error, it means that the port required to run Ogar is being used. Usually, Skype is the culprit. To solve this, either close out skype, or change the serverPort value in gameserver.ini to a different port. You will have to change your connection ip to "127.0.0.1:PORT"** - -Once the game server is running, you can connect (locally) by typing `agar.io/?ip=127.0.0.1:443` into your browser's address bar. - -## Configuring Ogar -Use "gameserver.ini" to modify Ogar's configurations field. Player bots are currently basic and for testing purposes. To use them, change "serverBots" to a value higher than zero in the configuration file. To add/remove bot names, edit the file named "botnames.txt" which is in the same folder as "gameserver.ini". Names should be separated by using the enter key. - -## Custom Game modes -Ogar has support for custom game modes. To switch between game modes, change the value of "serverGamemode" in the configurations file to the selected game mode id and restart the server. The current supported game modes are: - -Id | Name ------|-------------- -0 | Free For All -1 | Teams -2 | Experimental (As of 6/13/15) -10 | Tournament -11 | Hunger Games -12 | Zombie Mode -13 | Team Z -14 | Team X -20 | Rainbow FFA - Hint: Use with "setAcid(true)" - -## Console Commands -The current available console commands are listed here. Command names are not case sensitive, but player names are. - - - Addbot [Number] - * Adds [Number] of bots to the server. If an amount is not specified, 1 bot will be added. - - Board [String 1] [String 2] [String 3] ... - * Replaces the text on the leaderboard with the string text. - - Boardreset - * Resets the leaderboard to display the proper data for the current gamemode - - Change [Config setting] [Value] - * Changes a config setting to a value. Ex. "change serverMaxConnections 32" will change the variable serverMaxConnections to 32. Note that some config values (Like serverGamemode) are parsed before the server starts so changing them mid game will have no effect. - - Clear - * Clears the console output - - Color [Player ID] [Red] [Green] [Blue] - * Replaces the color of the specified player with this color. - - Exit - * Closes the server. - - Food [X position] [Y position] [Mass] - * Spawns a food cell at those coordinates. If a mass value is not specified, then the server will default to "foodStartMass" in the config. - - Gamemode [Id] - * Changes the gamemode of the server. Warning - This can cause problems. - - Help - * Shows List Of Commands - - Kick [Player ID] - * Kicks the specified player or bot from the server. - - Kill [Player ID] - * Kills all cells belonging to the specified player. - - Killall - * Kills all player cells on the map. - - Mass [Player ID] [Number] - * Sets the mass of all cells belonging to the specified player to [Number]. - - Name [Player ID] [New Name] - * Changes the name of the player with the specified id with [New Name]. - - Playerlist - * Shows a list of connected players, their IP, player ID, the amount of cells they have, total mass, and their position. - - Pause - * Pauses/Unpauses the game. - - Reload - * Reloads the config file used by the server. However, the following values are not affected: serverPort, serverGamemode, serverBots, serverStatsPort, serverStatsUpdate. - - Status - * Shows the amount of players currently connected, time elapsed, memory usage (memory used/memory allocated), and the current gamemode. - - Tp [Player ID] [X position] [Y position] - * Teleports the specified player to the specified coordinates. - - Virus [X position] [Y position] [Mass] - * Spawns a virus cell at those coordinates. If a mass value is not specified, then the server will default to "virusStartMass" in the config. - -## Contributing -Please see [CONTRIBUTING.md](https://github.com/OgarProject/Ogar/blob/master/CONTRIBUTING.md) for contribution guidelines. - -## License -Please see [LICENSE.md](https://github.com/OgarProject/Ogar/blob/master/LICENSE.md). +# MultiOgar +Ogar game server with fast and smooth vanilla physics and multi-protocol support. + +Current version: **1.2.69** + +## Project Info +![Language](https://img.shields.io/badge/language-node.js-yellow.svg) +[![License](https://img.shields.io/badge/license-APACHE2-blue.svg)](https://github.com/Barbosik/OgarMulti/blob/master/LICENSE.md) + +MultiOgar code based on Ogar code that I heavily modified, and will continue to update. +Almost all physics and protocol code were rewritten and optimized. +The [OgarProject](https://ogarproject.com) owns Ogar, and I do not claim it as mine! +Original Ogar found [here](https://github.com/OgarProject/Ogar) + + +The goal is to make good and smooth physics and cleanup the code. + +## Ogar Server Tracker + +You can found active Ogar servers on http://ogar-tracker.tk +It updates server information in realtime with no need to refresh the page. + +If you want to include your server in the list. Just install the latest version of MultiOgar server and enable server tracking with `serverTracker = 1` in gameserver.ini + +If you have other server and want to include it in the list, just insert the code to ping ogar-tracker.tk into your server. +You can found example in MultiOgar source code: https://github.com/Barbosik/MultiOgar/blob/master/src/GameServer.js#L1799-L1823 + + +## Screenshot + +MultiOgar console: + +![Screenshot](https://i.imgur.com/GiJURq0.png) + +Version 1.2.8: +* 1000 bots, 500 viruses, 1000 foods, map 14142x14142 +* Works very-very smooth (with a little slower speed, but it will not be noticed by user). +* CPU load: 14% (x4 core) +* Memory usage: 70 MB + +![Screenshot](http://i.imgur.com/XsXjT0o.png) + + +## Install + +#### Windows: +* Download and install node.js: https://nodejs.org/en/download/ (64-bit recommended) +* Download MultiOgar code: https://github.com/Barbosik/MultiOgar/archive/master.zip +* Unzip MultiOgar code into some folder +* Start command line and execute from MultiOgar folder +``` +npm install +``` +and run the server: +``` +cd src +node index.js +``` + +#### Linux: +``` +# First update your packages: +sudo apt-get update + +# Install git: +sudo apt-get install git + +# Install node.js: +sudo apt-get install nodejs-legacy npm + +# Clone MultiOgar: +git clone git://github.com/Barbosik/MultiOgar.git + +# Install dependencies: +cd MultiOgar +npm install + +# Run the server: +cd src +sudo node index.js +``` + + +## Clients + +This lists Ogar clients and server trackers that I found on internet. + +###Ogar server trackers + +Welcome to http://ogar-tracker.tk :) + +URL | Description +--- | --- +http://ogar-tracker.tk | Ogar tracker +http://ogar.mivabe.nl/master | MivaBe, tracks a lot of servers +http://c0nsume.me/tracker.php | c0nsume.me server tracker + +Now you can allow MultiOgar to be listed on a server tracker. +Just set `serverTracker = 1` in the gameserver.ini, and your server will appear +on these pages: http://ogar.mivabe.nl/master , http://c0nsume.me/tracker.php +If you don't want to include your server to tracker list, +just set `serverTracker = 0` and the server will not ping the server tracker. + + +###Ogar clients +Just replace `127.0.0.1:443` in the url to the server IP and port to play. + +URL | Protocol | Description +--- | --- | --- +http://agar.io/?ip=127.0.0.1:443 | 8 | Vanilla +http://ogar.mivabe.nl/?ip=127.0.0.1:443 | early 5 | MivaBe, pretty smooth, custom graphics (anime) +http://play.ogarul.tk/?ip=127.0.0.1:443 | 4 | OgarUL, vanilla style +http://c0nsume.me/private4.php?ip=127.0.0.1:443 | 5 | vanilla style + +###MultiOgar Servers + +IP | Location | Game Mode | Web Site +--- | --- | --- | --- +bubble-wars.tk:4444 | France | FFA | http://agar.io/?ip=bubble-wars.tk:4444 (Test server) +bubble-wars.tk:4445 | France | FFA IM | http://agar.io/?ip=bubble-wars.tk:4445 (Test server) +vps.simonorj.com:24270 | Montreal | Instant Merge | https://redd.it/4mufge +164.132.48.230:600 | France | FFA | http://c0nsume.me/private4.php?ip=164.132.48.230:600 +149.202.87.51:443 | Paris | FFA | http://agarlist.com/ +134.119.17.230:443 | Germany | FFA | http://agarlist.com/ +192.34.61.57:443 | New York | FFA | http://agarlist.com/ + + +## What's new: +* 1.2.47: Improved stability and performance; added mute/unmute command +* Added support for secure websocket connections (TLS) +* Fixed mass decay +* Added ejectSizeLoss +* Added sub-net ban feature (use `ban xx.xx.xx.*` or `ban xx.xx.*.*` to ban entire sub-network) +* Added performance optimizations, now up to 700 bots with no lags at all +* Fixed bug when some cell split/eject were shown with delay for some clients +* Added a lot of protocol optimizations, now server works with no lags at all even with 64 connected players +* Added server version, now you can check if your MultiOgar code is fresh +* Significant performance improvement and more smooth physics +* Added protocol optimizations to reduce lags on cell multi split +* Fixed pop-split behavior +* Added spectate walk through feature (use Space key in spectate mode to lock the current player or to lock the next one. Use key Q to reset into the normal mode. Locked player is highlighted on leaderboard) +* Fixed cell-split order, now split-run works ok +* A little performance improvement for split/eject +* Fixed min mass to split/eject +* Fixed mass-limit behavior +* Added chat player commands /skin and /kill (to change skin, just type /skin %shark in the chat) +* Added scramble level 3 (anti-bot/anti-minimap protection), unsupported on some clients (unfortunately include vanilla, ogar.mivabe.nl works ok) +* NOTE: there is major gameserver.ini change, previous version is incompatible! +* Massive perfromance improvement & reduce network traffic +* Split behavior - fixed; +* Protocol code - optimized; +* Massive performance improvement with quad-tree lookup; +* Split/Eject - physics code rewritten; +* Player speed - physics code rewritten; +* Cell remerge - physics code rewritten; +* Cell collision - physics code rewritten; +* View area - code rewritten; +* Spectate - code rewritten; +* Mouse control and cell movements - physics code rewritten; +* Border calculations - rewritten; +* Border bouncy physics - fixed and improved; +* mainLoop - cleaned; +* Added support for different protocols (4, early 5, late 5, 6, 7, 8); +* Added automatic mouse message type recognition; +* Added chat support; +* Added anti-spam protection; +* Added skin support (use name "< shark > Fish", remove space); +* Color generator replaced with hsv model; +* Memory leaks - fixed; +* Performance improved and optimized +* Added support for server tracker ogar.mivabe.nl/master + +Most of the physics code from the original Ogar were rewritten. +The physics engine in MultiOgar is pretty close to the old vanilla physics. diff --git a/ogar-linux-script.sh b/ogar-linux-script.sh index fb4160072..48dac8db5 100644 --- a/ogar-linux-script.sh +++ b/ogar-linux-script.sh @@ -9,11 +9,11 @@ download_and_extract () { if [ ! -d Ogar-master ]; then if [ ! -f master.tar.gz ]; then echo "No local master.tar.gz found, downloading with curl." - curl -O -L https://github.com/forairan/Ogar/archive/master.tar.gz + curl -O -L https://github.com/Barbosik/MultiOgar/archive/master.tar.gz fi if [ ! -f master.tar.gz ]; then echo "curl failed to download master.tar.gz, trying wget." - wget https://github.com/forairan/Ogar/archive/master.tar.gz + wget https://github.com/Barbosik/MultiOgar/archive/master.tar.gz if [ ! -f master.tar.gz ]; then echo "wget failed as well. Aborting!" exit 1 diff --git a/package.json b/package.json index e3bc3883c..11b96a1d0 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,25 @@ -{ - "name": "Ogar", - "version": "1.0.0", - "description": "Open source Agar.io server", - "main": "src/index.js", - "dependencies": { - "ws": "latest", - "vector2-node": "latest" - }, - "devDependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "https://github.com/OgarProject/Ogar" - }, - "author": "Devin Ryan (http://devin.codes/)", - "license": "Apache License 2.0", - "bugs": { - "url": "https://github.com/OgarProject/Ogar/issues" - }, - "homepage": "https://github.com/OgarProject/Ogar" +{ + "name": "MultiOgar", + "version": "1.2.69", + "description": "Open source Ogar server", + "author": "Barbosik (https://github.com/Barbosik/MultiOgar)", + "homepage": "https://github.com/Barbosik/MultiOgar", + "license": "Apache-2.0", + "main": "src/index.js", + "dependencies": { + "quad-node": "^1.0.5", + "vector2-node": "latest", + "ws": "latest" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/Barbosik/MultiOgar" + }, + "bugs": { + "url": "https://github.com/Barbosik/MultiOgar/issues" + } } diff --git a/src/GameServer.js b/src/GameServer.js index b8782e252..87eb67565 100644 --- a/src/GameServer.js +++ b/src/GameServer.js @@ -1,1070 +1,1884 @@ -// Library imports -var WebSocket = require('ws'); -var http = require('http'); -var fs = require("fs"); -var ini = require('./modules/ini.js'); - -// Project imports -var Packet = require('./packet'); -var PlayerTracker = require('./PlayerTracker'); -var PacketHandler = require('./PacketHandler'); -var Entity = require('./entity'); -var Gamemode = require('./gamemodes'); -var BotLoader = require('./ai/BotLoader'); -var Logger = require('./modules/log'); - -// GameServer implementation -function GameServer() { - // Startup - this.run = true; - this.lastNodeId = 1; - this.lastPlayerId = 1; - this.clients = []; - this.largestClient; // Required for spectators - - this.nodes = []; - this.nonPlayerNodes = []; // All nodes except player nodes - this.nodesVirus = []; // Virus nodes - this.nodesEjected = []; // Ejected mass nodes - this.nodesPlayer = []; // Nodes controlled by players - - this.currentFood = 0; - this.leaderboard = []; - - this.bots = new BotLoader(this); - this.log = new Logger(); - this.commands; // Command handler - - // Main loop tick - this.time = +new Date; - this.startTime = this.time; - this.tick = 0; // 1 ms, 25 ms - collision update, next time all update - this.fullTick = 0; // 2 = all update - this.tickMain = 0; // 50 ms ticks, 20 of these = 1 leaderboard update - this.tickSpawn = 0; // Used with spawning food - - // Config - this.config = { // Border - Right: X increases, Down: Y increases (as of 2015-05-20) - serverMaxConnections: 64, // Maximum amount of connections to the server. - serverPort: 443, // Server port - serverGamemode: 0, // Gamemode, 0 = FFA, 1 = Teams - serverBots: 0, // Amount of player bots to spawn - serverViewBaseX: 1024, // Base view distance of players. Warning: high values may cause lag - serverViewBaseY: 592, - serverStatsPort: 88, // Port for stats server. Having a negative number will disable the stats server. - serverStatsUpdate: 60, // Amount of seconds per update for the server stats - serverLogLevel: 1, // Logging level of the server. 0 = No logs, 1 = Logs the console, 2 = Logs console and ip connections - serverScrambleCoords: 1, // Toggles scrambling of coordinates. 0 = No scrambling, 1 = scrambling. Default is 1. - serverScrambleMinimaps: 1, // Toggles scrambling of borders to render maps unusable. 0 = No scrambling, 1 = scrambling. Default is 1. - serverTeamingAllowed: 1, // Toggles anti-teaming. 0 = Anti-team enabled, 1 = Anti-team disabled - serverMaxLB: 10, // Controls the maximum players displayed on the leaderboard. - borderLeft: 0, // Left border of map (Vanilla value: 0) - borderRight: 6000, // Right border of map (Vanilla value: 14142.135623730952) - borderTop: 0, // Top border of map (Vanilla value: 0) - borderBottom: 6000, // Bottom border of map (Vanilla value: 14142.135623730952) - spawnInterval: 20, // The interval between each food cell spawn in ticks (1 tick = 50 ms) - foodSpawnAmount: 10, // The amount of food to spawn per interval - foodStartAmount: 100, // The starting amount of food in the map - foodMaxAmount: 500, // Maximum food cells on the map - foodMass: 1, // Starting food size (In mass) - foodMassGrow: 1, // Enable food mass grow ? - foodMassGrowPossiblity: 50, // Chance for a food to has the ability to be self growing - foodMassLimit: 5, // Maximum mass for a food can grow - foodMassTimeout: 120, // The amount of interval for a food to grow its mass (in seconds) - virusMinAmount: 10, // Minimum amount of viruses on the map. - virusMaxAmount: 50, // Maximum amount of viruses on the map. If this amount is reached, then ejected cells will pass through viruses. - virusStartMass: 100, // Starting virus size (In mass) - virusFeedAmount: 7, // Amount of times you need to feed a virus to shoot it - ejectMass: 13, // Mass of ejected cells - ejectMassCooldown: 100, // Time until a player can eject mass again - ejectMassLoss: 15, // Mass lost when ejecting cells - ejectSpeed: 100, // Base speed of ejected cells - ejectSpawnPlayer: 50, // Chance for a player to spawn from ejected mass - playerStartMass: 10, // Starting mass of the player cell. - playerBotGrowEnabled: 1, // If 0, eating a cell with less than 17 mass while cell has over 625 wont gain any mass - playerMaxMass: 22500, // Maximum mass a player can have - playerMinMassEject: 32, // Mass required to eject a cell - playerMinMassSplit: 36, // Mass required to split - playerMaxCells: 16, // Max cells the player is allowed to have - playerRecombineTime: 30, // Base amount of seconds before a cell is allowed to recombine - playerMassAbsorbed: 1.0, // Fraction of player cell's mass gained upon eating - playerMassDecayRate: .002, // Amount of mass lost per second - playerMinMassDecay: 9, // Minimum mass for decay to occur - playerMaxNickLength: 15, // Maximum nick length - playerSpeed: 30, // Player base speed - playerDisconnectTime: 60, // The amount of seconds it takes for a player cell to be removed after disconnection (If set to -1, cells are never removed) - tourneyMaxPlayers: 12, // Maximum amount of participants for tournament style game modes - tourneyPrepTime: 10, // Amount of ticks to wait after all players are ready (1 tick = 1000 ms) - tourneyEndTime: 30, // Amount of ticks to wait after a player wins (1 tick = 1000 ms) - tourneyTimeLimit: 20, // Time limit of the game, in minutes. - tourneyAutoFill: 0, // If set to a value higher than 0, the tournament match will automatically fill up with bots after this amount of seconds - tourneyAutoFillPlayers: 1, // The timer for filling the server with bots will not count down unless there is this amount of real players - }; - // Parse config - this.loadConfig(); - - // Gamemodes - this.gameMode = Gamemode.get(this.config.serverGamemode); -} - -module.exports = GameServer; - -GameServer.prototype.start = function() { - // Logging - this.log.setup(this); - - // Gamemode configurations - this.gameMode.onServerInit(this); - - // Start the server - this.socketServer = new WebSocket.Server({ - port: this.config.serverPort, - perMessageDeflate: false - }, function() { - // Spawn starting food - this.startingFood(); - - // Start Main Loop - setInterval(this.mainLoop.bind(this), 1); - - // Done - console.log("[Game] Listening on port " + this.config.serverPort); - console.log("[Game] Current game mode is " + this.gameMode.name); - - // Player bots (Experimental) - if (this.config.serverBots > 0) { - for (var i = 0; i < this.config.serverBots; i++) { - this.bots.addBot(); - } - console.log("[Game] Loaded " + this.config.serverBots + " player bots"); - } - - }.bind(this)); - - this.socketServer.on('connection', connectionEstablished.bind(this)); - - // Properly handle errors because some people are too lazy to read the readme - this.socketServer.on('error', function err(e) { - switch (e.code) { - case "EADDRINUSE": - console.log("[Error] Server could not bind to port! Please close out of Skype or change 'serverPort' in gameserver.ini to a different number."); - break; - case "EACCES": - console.log("[Error] Please make sure you are running Ogar with root privileges."); - break; - default: - console.log("[Error] Unhandled error code: " + e.code); - break; - } - process.exit(1); // Exits the program - }); - - function connectionEstablished(ws) { - if (this.clients.length >= this.config.serverMaxConnections) { // Server full - ws.close(); - return; - } - - // ----- Client authenticity check code ----- - // !!!!! WARNING !!!!! - // THE BELOW SECTION OF CODE CHECKS TO ENSURE THAT CONNECTIONS ARE COMING - // FROM THE OFFICIAL AGAR.IO CLIENT. IF YOU REMOVE OR MODIFY THE BELOW - // SECTION OF CODE TO ALLOW CONNECTIONS FROM A CLIENT ON A DIFFERENT DOMAIN, - // YOU MAY BE COMMITTING COPYRIGHT INFRINGEMENT AND LEGAL ACTION MAY BE TAKEN - // AGAINST YOU. THIS SECTION OF CODE WAS ADDED ON JULY 9, 2015 AT THE REQUEST - // OF THE AGAR.IO DEVELOPERS. - var origin = ws.upgradeReq.headers.origin; - if (origin != 'http://agar.io' && - origin != 'https://agar.io' && - origin != 'http://localhost' && - origin != 'https://localhost' && - origin != 'http://127.0.0.1' && - origin != 'https://127.0.0.1') { - - ws.close(); - return; - } - // -----/Client authenticity check code ----- - - function close(error) { - // Log disconnections - this.server.log.onDisconnect(this.socket.remoteAddress); - - var client = this.socket.playerTracker; - var len = this.socket.playerTracker.cells.length; - for (var i = 0; i < len; i++) { - var cell = this.socket.playerTracker.cells[i]; - - if (!cell) { - continue; - } - - cell.calcMove = function() { - return; - }; // Clear function so that the cell cant move - //this.server.removeNode(cell); - } - - client.disconnect = this.server.config.playerDisconnectTime * 20; - this.socket.sendPacket = function() { - return; - }; // Clear function so no packets are sent - } - - ws.remoteAddress = ws._socket.remoteAddress; - ws.remotePort = ws._socket.remotePort; - this.log.onConnect(ws.remoteAddress); // Log connections - - ws.playerTracker = new PlayerTracker(this, ws); - ws.packetHandler = new PacketHandler(this, ws); - ws.on('message', ws.packetHandler.handleMessage.bind(ws.packetHandler)); - - var bindObject = { - server: this, - socket: ws - }; - ws.on('error', close.bind(bindObject)); - ws.on('close', close.bind(bindObject)); - this.clients.push(ws); - } - - this.startStatsServer(this.config.serverStatsPort); -}; - -GameServer.prototype.getMode = function() { - return this.gameMode; -}; - -GameServer.prototype.getNextNodeId = function() { - // Resets integer - if (this.lastNodeId > 2147483647) { - this.lastNodeId = 1; - } - return this.lastNodeId++; -}; - -GameServer.prototype.getNewPlayerID = function() { - // Resets integer - if (this.lastPlayerId > 2147483647) { - this.lastPlayerId = 1; - } - return this.lastPlayerId++; -}; - -GameServer.prototype.getRandomPosition = function() { - var xSum = this.config.borderRight + this.config.borderLeft; - var ySum = this.config.borderBottom + this.config.borderTop; - return { - x: Math.floor(Math.random() * xSum - this.config.borderLeft), - y: Math.floor(Math.random() * ySum - this.config.borderTop) - }; -}; - -GameServer.prototype.getRandomSpawn = function(mass) { - // Random and secure spawns for players and viruses - var pos = this.getRandomPosition(); - var unsafe = this.willCollide(mass, pos, mass == this.config.virusStartMass); - var attempt = 1; - - // Prevent stack overflow by counting attempts - while (true) { - if (!unsafe || attempt >= 15) break; - pos = this.getRandomPosition(); - unsafe = this.willCollide(mass, pos, mass == this.config.virusStartMass); - attempt++; - } - - // If it reached attempt 15, warn the user - if (attempt >= 14) { - console.log("[Server] Entity was force spawned near viruses/playercells after 15 attempts."); - console.log("[Server] If this message keeps appearing, check your config, especially start masses for players and viruses."); - } - - return pos; -}; - -GameServer.prototype.getRandomColor = function() { - var colorRGB = [0xFF, 0x07, (Math.random() * 256) >> 0]; - colorRGB.sort(function() { - return 0.5 - Math.random(); - }); - return { - r: colorRGB[0], - g: colorRGB[1], - b: colorRGB[2] - }; -}; - -GameServer.prototype.addNode = function(node) { - this.nodes.push(node); - if (node.cellType != 0) this.nonPlayerNodes.push(node); - - // Adds to the owning player's screen excluding ejected cells - if (node.owner && node.cellType != 3) { - node.setColor(node.owner.color); - node.owner.cells.push(node); - node.owner.socket.sendPacket(new Packet.AddNode(node)); - } - - // Special on-add actions - node.onAdd(this); - - // Add to visible nodes - for (var i = 0; i < this.clients.length; i++) { - var client = this.clients[i].playerTracker; - if (!client) { - continue; - } - - // client.nodeAdditionQueue is only used by human players, not bots - // for bots it just gets collected forever, using ever-increasing amounts of memory - if ('_socket' in client.socket && node.visibleCheck(client.viewBox, client.centerPos, client.cells)) { - client.nodeAdditionQueue.push(node); - } - } -}; - -GameServer.prototype.removeNode = function(node) { - // Remove from main nodes list - var index = this.nodes.indexOf(node); - if (index != -1) { - this.nodes.splice(index, 1); - } - - if (node.cellType != 0) { - // Remove from non-player node list - index = this.nonPlayerNodes.indexOf(node); - if (index != -1) { - this.nonPlayerNodes.splice(index, 1); - } - } - - // Special on-remove actions - node.onRemove(this); - - // Animation when eating - for (var i = 0; i < this.clients.length; i++) { - var client = this.clients[i].playerTracker; - if (!client) continue; - - // Remove from client - client.nodeDestroyQueue.push(node); - } -}; - -GameServer.prototype.moveTick = function() { - // Move cells - this.updateMoveEngine(); -}; - -GameServer.prototype.spawnTick = function() { - // Spawn food - this.tickSpawn++; - if (this.tickSpawn >= this.config.spawnInterval) { - this.updateFood(); // Spawn food - this.virusCheck(); // Spawn viruses - - this.tickSpawn = 0; // Reset - } -}; - -GameServer.prototype.gamemodeTick = function() { - // Gamemode tick - this.gameMode.onTick(this); -}; - -GameServer.prototype.cellUpdateTick = function() { - // Update cells - this.updateCells(); -}; - -GameServer.prototype.mainLoop = function() { - // Timer - var local = new Date(); - this.tick += (local - this.time); - this.time = local; - - if (!this.run) return; - - // The node & client updating mechanism is perfomance overhauled - // Nodes & clients will update periodically and never all on 25ms/50ms - // PlayerTracker and all Cell types have own internal update timer which is measured - setTimeout(this.updateClients.bind(this), 0); - setTimeout(this.moveTick.bind(this), 0); - - - if (this.tick >= 25) { - this.fullTick++; - - if (this.fullTick >= 2) { - // Loop main functions - setTimeout(this.spawnTick.bind(this), 0); - setTimeout(this.gamemodeTick.bind(this), 0); - setTimeout(this.cellUpdateTick.bind(this), 0); - - // Update cells/leaderboard loop - this.tickMain++; - if (this.tickMain >= 4) { // 250 milliseconds - // Update leaderboard with the gamemode's method - this.leaderboard = []; - this.gameMode.updateLB(this); - - if (!this.gameMode.specByLeaderboard) { - // Get client with largest score if gamemode doesn't have a leaderboard - var clients = this.clients.valueOf(); - - // Use sort function - clients.sort(function(a, b) { - return b.playerTracker.getScore(true) - a.playerTracker.getScore(true); - }); - this.largestClient = clients[0].playerTracker; - } else this.largestClient = this.gameMode.rankOne; - - this.tickMain = 0; // Reset - } - this.fullTick = 0; // Reset - } - - // Debug - //console.log(this.tick - 25); - - // Reset - this.tick = 0; - } -}; - -GameServer.prototype.updateClients = function() { - // The node & client updating mechanism is perfomance overhauled - // Nodes & clients will update periodically and never all on 25ms/50ms - // PlayerTracker and all Cell types have own internal update timer which is measured - - var updatedClients = []; - - var len = this.clients.length; - for (var i = 0; i < len; i++) { - var client = this.clients[i].playerTracker; - if (!client) continue; - - client.ticksLeft--; - if (client.ticksLeft > 0) continue; - updatedClients.push(client); - - client.ticksLeft = 40; // Reset timer - - client.update(); - client.antiTeamTick(); - } - - // Very experimental and currently will freeze the server. - // If there are too many updated clients at once, update them a few ticks later - /*var maxOnTick = Math.ceil(this.clients.length / 50); - if (updatedClients.length > maxOnTick) { - for (var i = 0; i < updatedClients.length - maxOnTick; i) { - updatedClients[i].ticksLeft += i + 1; - } - }*/ -}; - -GameServer.prototype.startingFood = function() { - // Spawns the starting amount of food cells - for (var i = 0; i < this.config.foodStartAmount; i++) { - this.spawnFood(); - } -}; - -GameServer.prototype.updateFood = function() { - var toSpawn = Math.min(this.config.foodSpawnAmount, (this.config.foodMaxAmount - this.currentFood)); - for (var i = 0; i < toSpawn; i++) { - this.spawnFood(); - } -}; - -GameServer.prototype.spawnFood = function() { - var f = new Entity.Food(this.getNextNodeId(), null, this.getRandomPosition(), this.config.foodMass, this); - f.setColor(this.getRandomColor()); - - this.addNode(f); - this.currentFood++; -}; - -GameServer.prototype.spawnPlayer = function(player, pos, mass) { - if (mass == null) { // Get starting mass - mass = this.config.playerStartMass; - } - if (pos == null) { // Get random pos - pos = this.getRandomSpawn(mass); - } - - // Spawn player and add to world - var cell = new Entity.PlayerCell(this.getNextNodeId(), player, pos, mass, this); - this.addNode(cell); - - // Set initial mouse coords - player.mouse = { - x: pos.x, - y: pos.y - }; -}; - -GameServer.prototype.virusCheck = function() { - // Checks if there are enough viruses on the map - if (this.nodesVirus.length < this.config.virusMinAmount) { - // Spawns a virus - var pos = this.getRandomSpawn(this.config.virusStartMass); - - var v = new Entity.Virus(this.getNextNodeId(), null, pos, this.config.virusStartMass, this); - this.addNode(v); - } -}; - -GameServer.prototype.willCollide = function(mass, pos, isVirus) { - // Look if there will be any collision with the current nodes - var size = Math.sqrt(mass * 100) >> 0; - - for (var i = 0; i < this.nodesPlayer.length; i++) { - var check = this.nodesPlayer[i]; - if (!check) continue; - - // Eating range - var xs = check.position.x - pos.x, - ys = check.position.y - pos.y, - sqDist = xs * xs + ys * ys, - dist = Math.sqrt(sqDist); - - if (check.getSize() > size) { // Check only if the player cell is larger than imaginary cell - if (dist + size <= check.getSize()) return true; // Collided - } - } - - if (isVirus) return false; // Don't check for viruses if the new cell will be virus - - for (var i = 0; i < this.nodesVirus.length; i++) { - var check = this.nodesVirus[i]; - if (!check) continue; - - // Eating range - var xs = check.position.x - pos.x, - ys = check.position.y - pos.y, - sqDist = xs * xs + ys * ys, - dist = Math.sqrt(sqDist); - - if (check.getSize() > size) { // Check only if the virus cell is larger than imaginary cell - if (dist + size <= check.getSize()) return true; // Collided - } - } - return false; -}; - -GameServer.prototype.getDist = function(x1, y1, x2, y2) { // Use Pythagoras theorem - var deltaX = x1 - x2; - var deltaY = y1 - y2; - return Math.sqrt(deltaX * deltaX + deltaY * deltaY); -}; - -GameServer.prototype.abs = function(x) { // Because Math.abs is slow - return x < 0 ? -x : x; -}; - -GameServer.prototype.checkCellCollision = function(cell, check) { - // Returns object which contains info about cell's collisions. You can use this in the future. - - // Check the two cells for collision - var collisionDist = cell.getSize() + check.getSize(); // Minimum distance between the two cells - - var dY = cell.position.y - check.position.y; - var dX = cell.position.x - check.position.x; - var angle = Math.atan2(dX, dY); - var dist = Math.sqrt(dX * dX + dY * dY); - - return ({ - cellDist: dist, - collideDist: collisionDist, - cellMult: (Math.sqrt(check.getSize() * 100) / Math.sqrt(cell.getSize() * 100)) / 3, - cellAngle: angle, - collided: (dist < collisionDist) - }); -}; - -GameServer.prototype.cellCollision = function(cell, check, calcInfo) { - if (!calcInfo) calcInfo = this.checkCellCollision(cell, check); // Unedefined calc info - - // Check collision - if (calcInfo.collided) { // Collided - // The moving cell pushes the colliding cell - - var dist = calcInfo.cellDist; - var collisionDist = calcInfo.collideDist; - var mult = calcInfo.cellMult; - var angle = calcInfo.cellAngle; - - var move = (collisionDist - dist) * mult; - - cell.position.x += move * Math.sin(angle); - cell.position.y += move * Math.cos(angle); - } -}; - -GameServer.prototype.updateMoveEngine = function() { - // Move player cells - var len = this.nodesPlayer.length; - - for (var i in this.clients) { - var client = this.clients[i].playerTracker; - - client.cellUpdateTick--; - if (client.cellUpdateTick > 0) continue; - client.cellUpdateTick = 18; - - // Sort client's cells by ascending mass - var sorted = []; - for (var i = 0; i < client.cells.length; i++) sorted.push(client.cells[i]); - - sorted.sort(function(a, b) { - return b.mass - a.mass; - }); - - // Go cell by cell - for (var i = 0; i < sorted.length; i++) { - var cell = sorted[i]; - - // Do not move cells that have already been eaten - if (!cell) { - continue; - } - - // First move the cell - cell.calcMovePhys(this.config); - - // Now move it to the mouse - cell.calcMove(client.mouse.x, client.mouse.y, this); - - // Collision with own cells - cell.collision(this); - - // Cell eating - this.cellEating(cell); - } - } - - - // A system to move cells not controlled by players (ex. viruses, ejected mass) - len = this.nonPlayerNodes.length; - for (var i = 0; i < len; i++) { - var node = this.nonPlayerNodes[i]; - if (!node) continue; - - node.updateTicks--; - if (node.updateTicks > 0) continue; - node.updateTicks = 25; - - if (node.moveEngineSpeed <= 0) continue; // No speed - node.calcMovePhys(this.config); - node.onAutoMove(this); - } -}; - -GameServer.prototype.cellEating = function(cell) { - // Check if cells nearby - var list = this.getCellsInRange(cell); - for (var j = 0; j < list.length; j++) { - var check = list[j]; - - // Consume effect - check.onConsume(cell, this); - - // Remove cell - check.setKiller(cell); - this.removeNode(check); - } -}; - -GameServer.prototype.splitCells = function(client) { - var len = client.cells.length; - var splitCells = 0; // How many cells have been split - for (var i = 0; i < len; i++) { - var cell = client.cells[i]; - - var deltaY = client.mouse.y - cell.position.y; - var deltaX = client.mouse.x - cell.position.x; - var angle = Math.atan2(deltaX, deltaY); - if (angle == 0) angle = Math.PI / 2; - - if (this.createPlayerCell(client, cell, angle, cell.mass / 2) == true) splitCells++; - } - if (splitCells > 0) client.applyTeaming(1, 2); // Account anti-teaming -}; - -GameServer.prototype.createPlayerCell = function(client, parent, angle, mass) { - // Returns boolean whether a cell has been split or not. You can use this in the future. - - if (client.cells.length >= this.config.playerMaxCells) { - // Player cell limit - return false; - } - - if (parent.mass < this.config.playerMinMassSplit) { - // Minimum mass to split - return false; - } - - // Calculate customized speed for splitting cells - var t = Math.PI * Math.PI; - var modifier = 3 + Math.log(1 + mass) / (10 + Math.log(1 + mass)); - var splitSpeed = this.config.playerSpeed * Math.min(Math.pow(mass, -Math.PI / t / 10) * modifier, 150); - - // Calculate new position - var newPos = { - x: parent.position.x, - y: parent.position.y - }; - - // Create cell - var newCell = new Entity.PlayerCell(this.getNextNodeId(), client, newPos, mass, this); - newCell.setAngle(angle); - newCell.setMoveEngineData(splitSpeed, 0.88); - // Cells won't collide immediately - newCell.collisionRestoreTicks = 12; - parent.collisionRestoreTicks = 12; - newCell.calcMergeTime(this.config.playerRecombineTime); - parent.mass -= mass; // Remove mass from parent cell - - // Add to node list - this.addNode(newCell); - return true; -}; - -GameServer.prototype.canEjectMass = function(client) { - if (typeof client.lastEject == 'undefined' || this.time - client.lastEject >= this.config.ejectMassCooldown) { - client.lastEject = this.time; - return true; - } else - return false; -}; - -GameServer.prototype.ejectMass = function(client) { - if (!this.canEjectMass(client)) - return; - for (var i = 0; i < client.cells.length; i++) { - var cell = client.cells[i]; - - if (!cell) { - continue; - } - - if (cell.mass < this.config.playerMinMassEject) { - continue; - } - - var deltaY = client.mouse.y - cell.position.y; - var deltaX = client.mouse.x - cell.position.x; - var angle = Math.atan2(deltaX, deltaY); - - // Randomize angle - angle += (Math.random() * 0.1) - 0.05; - - // Get starting position - var size = cell.getSize() + 0.2; - var startPos = { - x: cell.position.x + ((size + this.config.ejectMass) * Math.sin(angle)), - y: cell.position.y + ((size + this.config.ejectMass) * Math.cos(angle)) - }; - - // Remove mass from parent cell - cell.mass -= this.config.ejectMassLoss; - - // Randomize angle - angle += (Math.random() * 0.6) - 0.3; - - // Create cell - var ejected = new Entity.EjectedMass(this.getNextNodeId(), client, startPos, this.config.ejectMass, this); - ejected.setAngle(angle); - ejected.setMoveEngineData(this.config.ejectSpeed, 0.88); - ejected.setColor(cell.getColor()); - - this.nodesEjected.push(ejected); - this.addNode(ejected); - } -}; - -GameServer.prototype.shootVirus = function(parent) { - var parentPos = { - x: parent.position.x, - y: parent.position.y, - }; - - var newVirus = new Entity.Virus(this.getNextNodeId(), null, parentPos, this.config.virusStartMass, this); - newVirus.setAngle(parent.getAngle()); - newVirus.setMoveEngineData(115, 0.9); - - // Add to moving cells list - this.addNode(newVirus); -}; - -GameServer.prototype.getCellsInRange = function(cell) { - var list = []; - var squareR = cell.getSquareSize(); // Get cell squared radius - - // Loop through all cells that are colliding with the player's cells - var len = cell.owner.collidingNodes.length; - for (var i = 0; i < len; i++) { - var check = cell.owner.collidingNodes[i]; - - if (typeof check === 'undefined') { - continue; - } - - // if something already collided with this cell, don't check for other collisions - if (check.inRange) { - continue; - } - - // Can't eat itself - if (cell.nodeId == check.nodeId) { - continue; - } - - // Can't eat cells that have collision turned off - if ((cell.owner == check.owner) && (cell.collisionRestoreTicks != 0) && (check.cellType == 0)) { - continue; - } - - // Eating range - var xs = cell.position.x - check.position.x, - ys = cell.position.y - check.position.y, - sqDist = xs * xs + ys * ys, - dist = Math.sqrt(sqDist); - - // Use a more reliant version for pellets - // Might be a bit slower but it can be eaten with any mass - if (check.cellType == 1) { - if (dist + check.getSize() / 3.14 > cell.getSize()) continue; // Too far away - else { - // Add to list of cells nearby - list.push(check); - - // Something is about to eat this cell; no need to check for other collisions with it - check.inRange = true; - continue; // No need to look for type and calculate if eaten again - } - } - - // Cell type check - Cell must be bigger than this number times the mass of the cell being eaten - var multiplier = 1.3; - - switch (check.getType()) { - case 0: // Players - // Can't eat self if it's not time to recombine yet - if (check.owner == cell.owner) { - // If one of cells can't merge - if (!cell.shouldRecombine || !check.shouldRecombine) { - // Check if merge command was triggered on this client - if (!cell.owner.mergeOverride) continue; - } - - multiplier = 1.0; - } - - // Can't eat team members - if (this.gameMode.haveTeams) { - if (!check.owner) { // Error check - continue; - } - - if ((check.owner != cell.owner) && (check.owner.getTeam() == cell.owner.getTeam())) { - continue; - } - } - break; - default: - break; - } - - // Make sure the cell is big enough to be eaten. - if ((check.mass * multiplier) > cell.mass) { - continue; - } - - // Eating range = radius of eating cell / 2 - 31% of the radius of the cell being eaten - var eatingRange = cell.getSize() - check.getEatingRange(); - - if (dist < eatingRange) { - // Add to list of cells nearby - list.push(check); - - // Something is about to eat this cell; no need to check for other collisions with it - check.inRange = true; - } else { - // Not in eating range - continue; - } - } - return list; -}; - -GameServer.prototype.getNearestVirus = function(cell) { - // More like getNearbyVirus - var virus = null; - var r = 100; // Checking radius - - var topY = cell.position.y - r; - var bottomY = cell.position.y + r; - - var leftX = cell.position.x - r; - var rightX = cell.position.x + r; - - // Loop through all viruses on the map. There is probably a more efficient way of doing this but whatever - var len = this.nodesVirus.length; - for (var i = 0; i < len; i++) { - var check = this.nodesVirus[i]; - - if (typeof check === 'undefined') { - continue; - } - - if (!check.collisionCheck(bottomY, topY, rightX, leftX)) { - continue; - } - - // Add to list of cells nearby - virus = check; - break; // stop checking when a virus found - } - return virus; -}; - -GameServer.prototype.updateCells = function() { - if (!this.run) { - // Server is paused - return; - } - - // Loop through all player cells - var massDecay = 1 - (this.config.playerMassDecayRate * this.gameMode.decayMod * 0.05); - for (var i = 0; i < this.nodesPlayer.length; i++) { - var cell = this.nodesPlayer[i]; - - if (!cell) { - continue; - } - - // Recombining - if (cell.owner.cells.length > 1) { - cell.recombineTicks += 0.05; - cell.calcMergeTime(this.config.playerRecombineTime); - } else if (cell.owner.cells.length == 1 && cell.recombineTicks > 0) { - cell.recombineTicks = 0; - cell.shouldRecombine = false; - cell.owner.mergeOverride = false; - cell.owner.mergeOverrideDuration = 0; - } - // Collision - if (cell.collisionRestoreTicks > 0) { - cell.collisionRestoreTicks--; - } - - // Mass decay - if (cell.mass >= this.config.playerMinMassDecay) { - var client = cell.owner; - if (this.config.serverTeamingAllowed == 0) { - var teamMult = (client.massDecayMult - 1) / 1111 + 1; // Calculate anti-teaming multiplier for decay - var thisDecay = 1 - massDecay * (1 / teamMult); // Reverse mass decay and apply anti-teaming multiplier - cell.mass *= (1 - thisDecay); - } else { - // No anti-team - cell.mass *= massDecay; - } - } - } -}; - -GameServer.prototype.loadConfig = function() { - try { - // Load the contents of the config file - var load = ini.parse(fs.readFileSync('./gameserver.ini', 'utf-8')); - - // Replace all the default config's values with the loaded config's values - for (var obj in load) { - this.config[obj] = load[obj]; - } - } catch (err) { - // No config - console.log("[Game] Config not found... Generating new config"); - - // Create a new config - fs.writeFileSync('./gameserver.ini', ini.stringify(this.config)); - } -}; - -// Stats server - -GameServer.prototype.startStatsServer = function(port) { - // Do not start the server if the port is negative - if (port < 1) { - return; - } - - // Create stats - this.stats = "Test"; - this.getStats(); - - // Show stats - this.httpServer = http.createServer(function(req, res) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.writeHead(200); - res.end(this.stats); - }.bind(this)); - - this.httpServer.listen(port, function() { - // Stats server - console.log("[Game] Loaded stats server on port " + port); - setInterval(this.getStats.bind(this), this.config.serverStatsUpdate * 1000); - }.bind(this)); -}; - -GameServer.prototype.getStats = function() { - var players = 0; - this.clients.forEach(function(client) { - if (client.playerTracker && client.playerTracker.cells.length > 0) - players++; - }); - var s = { - 'current_players': this.clients.length, - 'alive': players, - 'spectators': this.clients.length - players, - 'max_players': this.config.serverMaxConnections, - 'gamemode': this.gameMode.name, - 'uptime': Math.round((new Date().getTime() - this.startTime)/1000/60)+" m", - 'start_time': this.startTime - }; - this.stats = JSON.stringify(s); -}; - -// Custom prototype functions -WebSocket.prototype.sendPacket = function(packet) { - function getBuf(data) { - var array = new Uint8Array(data.buffer || data); - var l = data.byteLength || data.length; - var o = data.byteOffset || 0; - var buffer = new Buffer(l); - - for (var i = 0; i < l; i++) { - buffer[i] = array[o + i]; - } - - return buffer; - } - - //if (this.readyState == WebSocket.OPEN && (this._socket.bufferSize == 0) && packet.build) { - if (this.readyState == WebSocket.OPEN && packet.build) { - var buf = packet.build(); - this.send(getBuf(buf), { - binary: true - }); - } else if (!packet.build) { - // Do nothing - } else { - this.readyState = WebSocket.CLOSED; - this.emit('close'); - this.removeAllListeners(); - } -}; +// Library imports +var http = require('http'); +var fs = require("fs"); +var os = require('os'); +var path = require('path'); +var pjson = require('../package.json'); +var ini = require('./modules/ini.js'); +var QuadNode = require('quad-node'); +var PlayerCommand = require('./modules/PlayerCommand'); +var HttpsServer = require('./HttpsServer'); + +// Project imports +var Packet = require('./packet'); +var PlayerTracker = require('./PlayerTracker'); +var PacketHandler = require('./PacketHandler'); +var Entity = require('./entity'); +var Gamemode = require('./gamemodes'); +var BotLoader = require('./ai/BotLoader'); +var Logger = require('./modules/Logger'); +var UserRoleEnum = require('./enum/UserRoleEnum'); + +// GameServer implementation +function GameServer() { + this.httpServer = null; + this.wsServer = null; + + // Startup + this.run = true; + this.lastNodeId = 1; + this.lastPlayerId = 1; + this.clients = []; + this.socketCount = 0; + this.largestClient; // Required for spectators + this.nodes = []; + this.nodesVirus = []; // Virus nodes + this.nodesEjected = []; // Ejected mass nodes + this.quadTree = null; + + this.currentFood = 0; + this.movingNodes = []; // For move engine + this.leaderboard = []; + this.leaderboardType = -1; // no type + + this.bots = new BotLoader(this); + this.commands; // Command handler + + // Main loop tick + this.startTime = Date.now(); + this.stepDateTime = 0; + this.timeStamp = 0; + this.updateTime = 0; + this.updateTimeAvg = 0; + this.timerLoopBind = null; + this.mainLoopBind = null; + + this.tickCounter = 0; + + this.setBorder(10000, 10000); + + // Config + this.config = { + logVerbosity: 4, // Console log level (0=NONE; 1=FATAL; 2=ERROR; 3=WARN; 4=INFO; 5=DEBUG) + logFileVerbosity: 5, // File log level + + serverTimeout: 300, // Seconds to keep connection alive for non-responding client + serverWsModule: 'ws', // WebSocket module: 'ws' or 'uws' (install npm package before using uws) + serverMaxConnections: 64, // Maximum number of connections to the server. (0 for no limit) + serverIpLimit: 4, // Maximum number of connections from the same IP (0 for no limit) + serverMinionIgnoreTime: 30, // minion detection disable time on server startup [seconds] + serverMinionThreshold: 10, // max connections within serverMinionInterval time period, which will not be marked as minion + serverMinionInterval: 1000, // minion detection interval [milliseconds] + serverPort: 443, // Server port + serverBind: '0.0.0.0', // Network interface binding + serverTracker: 0, // Set to 1 if you want to show your server on the tracker http://ogar.mivabe.nl/master + serverGamemode: 0, // Gamemode, 0 = FFA, 1 = Teams + serverBots: 0, // Number of player bots to spawn + serverViewBaseX: 1920, // Base client screen resolution. Used to calculate view area. Warning: high values may cause lag + serverViewBaseY: 1080, // min value is 1920x1080 + serverMinScale: 0.15, // Min scale for player (low value leads to lags due to large visible area) + serverSpectatorScale: 0.4, // Scale (field of view) used for free roam spectators (low value leads to lags, vanilla=0.4, old vanilla=0.25) + serverStatsPort: 88, // Port for stats server. Having a negative number will disable the stats server. + serverStatsUpdate: 60, // Update interval of server stats in seconds + serverScrambleLevel: 2, // Toggles scrambling of coordinates. 0 = No scrambling, 1 = lightweight scrambling. 2 = full scrambling (also known as scramble minimap); 3 - high scrambling (no border) + serverMaxLB: 10, // Controls the maximum players displayed on the leaderboard. + serverChat: 1, // Set to 1 to allow chat; 0 to disable chat. + serverChatAscii: 1, // Set to 1 to disable non-ANSI letters in the chat (english only mode) + + serverName: 'MultiOgar #1', // Server name + serverWelcome1: 'Welcome to MultiOgar server!', // First server welcome message + serverWelcome2: '', // Second server welcome message (for info, etc) + + borderWidth: 14142, // Map border size (Vanilla value: 14142) + borderHeight: 14142, // Map border size (Vanilla value: 14142) + + foodMinSize: 10, // Minimum food size (vanilla 10) + foodMaxSize: 20, // Maximum food size (vanilla 20) + foodMinAmount: 1000, // Minimum food cells on the map + foodMaxAmount: 2000, // Maximum food cells on the map + foodSpawnAmount: 30, // The number of food to spawn per interval + foodMassGrow: 1, // Enable food mass grow ? + spawnInterval: 20, // The interval between each food cell spawn in ticks (1 tick = 50 ms) + + virusMinSize: 100, // Minimum virus size (vanilla 100) + virusMaxSize: 140, // Maximum virus size (vanilla 140) + virusMinAmount: 50, // Minimum number of viruses on the map. + virusMaxAmount: 100, // Maximum number of viruses on the map. If this number is reached, then ejected cells will pass through viruses. + + ejectSize: 38, // Size of ejected cells (vanilla 38) + ejectSizeLoss: 43, // Eject size which will be substracted from player cell (vanilla 43?) + ejectDistance: 780, // vanilla 780 + ejectCooldown: 3, // min ticks between ejects + ejectSpawnPlayer: 1, // if 1 then player may be spawned from ejected mass + + playerMinSize: 32, // Minimym size of the player cell (mass = 32*32/100 = 10.24) + playerMaxSize: 1500, // Maximum size of the player cell (mass = 1500*1500/100 = 22500) + playerMinSplitSize: 60, // Minimum player cell size allowed to split (mass = 60*60/100 = 36) + playerStartSize: 64, // Start size of the player cell (mass = 64*64/100 = 41) + playerMaxCells: 16, // Max cells the player is allowed to have + playerSpeed: 1, // Player speed multiplier + playerDecayRate: .002, // Amount of player cell size lost per second + playerRecombineTime: 30, // Base time in seconds before a cell is allowed to recombine + playerMaxNickLength: 15, // Maximum nick length + playerDisconnectTime: 60, // The time in seconds it takes for a player cell to be removed after disconnection (If set to -1, cells are never removed) + + tourneyMaxPlayers: 12, // Maximum number of participants for tournament style game modes + tourneyPrepTime: 10, // Number of ticks to wait after all players are ready (1 tick = 1000 ms) + tourneyEndTime: 30, // Number of ticks to wait after a player wins (1 tick = 1000 ms) + tourneyTimeLimit: 20, // Time limit of the game, in minutes. + tourneyAutoFill: 0, // If set to a value higher than 0, the tournament match will automatically fill up with bots after this amount of seconds + tourneyAutoFillPlayers: 1, // The timer for filling the server with bots will not count down unless there is this amount of real players + }; + + this.ipBanList = []; + this.minionTest = []; + this.userList = []; + this.badWords = []; + + // Parse config + this.loadConfig(); + this.loadIpBanList(); + this.loadUserList(); + this.loadBadWords(); + + this.setBorder(this.config.borderWidth, this.config.borderHeight); + this.quadTree = new QuadNode(this.border, 64, 32); + + // Gamemodes + this.gameMode = Gamemode.get(this.config.serverGamemode); +} + +module.exports = GameServer; + +GameServer.prototype.start = function () { + this.timerLoopBind = this.timerLoop.bind(this); + this.mainLoopBind = this.mainLoop.bind(this); + + // Gamemode configurations + this.gameMode.onServerInit(this); + + var dirSsl = path.join(path.dirname(module.filename), '../ssl'); + var pathKey = path.join(dirSsl, 'key.pem'); + var pathCert = path.join(dirSsl, 'cert.pem'); + + if (fs.existsSync(pathKey) && fs.existsSync(pathCert)) { + // HTTP/TLS + var options = { + key: fs.readFileSync(pathKey, 'utf8'), + cert: fs.readFileSync(pathCert, 'utf8') + }; + Logger.info("TLS: supported"); + this.httpServer = HttpsServer.createServer(options); + } else { + // HTTP only + Logger.warn("TLS: not supported (SSL certificate not found!)"); + this.httpServer = http.createServer(); + } + var wsOptions = { + server: this.httpServer, + perMessageDeflate: false, + maxPayload: 4096 + }; + Logger.info("WebSocket: " + this.config.serverWsModule); + WebSocket = require(this.config.serverWsModule); + // Custom prototype functions^M + WebSocket.prototype.sendPacket = function (packet) { + if (packet == null) return; + if (this.readyState == WebSocket.OPEN) { + if (this._socket.writable != null && !this._socket.writable) { + return; + } + var buffer = packet.build(this.playerTracker.socket.packetHandler.protocol); + if (buffer != null) { + this.send(buffer, { binary: true }); + } + } else { + this.readyState = WebSocket.CLOSED; + this.emit('close'); + } + }; + + this.wsServer = new WebSocket.Server(wsOptions); + this.wsServer.on('error', this.onServerSocketError.bind(this)); + this.wsServer.on('connection', this.onClientSocketOpen.bind(this)); + this.httpServer.listen(this.config.serverPort, this.config.serverBind, this.onHttpServerOpen.bind(this)); + + this.startStatsServer(this.config.serverStatsPort); +}; + +GameServer.prototype.onHttpServerOpen = function () { + // Spawn starting food + this.startingFood(); + + // Start Main Loop + setTimeout(this.timerLoopBind, 1); + + // Done + Logger.info("Listening on port " + this.config.serverPort); + Logger.info("Current game mode is " + this.gameMode.name); + + // Player bots (Experimental) + if (this.config.serverBots > 0) { + for (var i = 0; i < this.config.serverBots; i++) { + this.bots.addBot(); + } + Logger.info("Added " + this.config.serverBots + " player bots"); + } +}; + +GameServer.prototype.onServerSocketError = function (error) { + Logger.error("WebSocket: " + error.code + " - " + error.message); + switch (error.code) { + case "EADDRINUSE": + Logger.error("Server could not bind to port " + this.config.serverPort + "!"); + Logger.error("Please close out of Skype or change 'serverPort' in gameserver.ini to a different number."); + break; + case "EACCES": + Logger.error("Please make sure you are running Ogar with root privileges."); + break; + } + process.exit(1); // Exits the program +}; + +GameServer.prototype.onClientSocketOpen = function (ws) { + var logip = ws._socket.remoteAddress + ":" + ws._socket.remotePort; + ws.on('error', function (err) { + Logger.writeError("[" + logip + "] " + err.stack); + }); + if (this.config.serverMaxConnections > 0 && this.socketCount >= this.config.serverMaxConnections) { + ws.close(1000, "No slots"); + return; + } + if (this.checkIpBan(ws._socket.remoteAddress)) { + ws.close(1000, "IP banned"); + return; + } + if (this.config.serverIpLimit > 0) { + var ipConnections = 0; + for (var i = 0; i < this.clients.length; i++) { + var socket = this.clients[i]; + if (!socket.isConnected || socket.remoteAddress != ws._socket.remoteAddress) + continue; + ipConnections++; + } + if (ipConnections >= this.config.serverIpLimit) { + ws.close(1000, "IP limit reached"); + return; + } + } + ws.isConnected = true; + ws.remoteAddress = ws._socket.remoteAddress; + ws.remotePort = ws._socket.remotePort; + ws.lastAliveTime = Date.now(); + Logger.write("CONNECTED " + ws.remoteAddress + ":" + ws.remotePort + ", origin: \"" + ws.upgradeReq.headers.origin + "\""); + + ws.playerTracker = new PlayerTracker(this, ws); + ws.packetHandler = new PacketHandler(this, ws); + ws.playerCommand = new PlayerCommand(this, ws.playerTracker); + + var self = this; + var onMessage = function (message) { + self.onClientSocketMessage(ws, message); + }; + var onError = function (error) { + self.onClientSocketError(ws, error); + }; + var onClose = function (reason) { + self.onClientSocketClose(ws, reason); + }; + ws.on('message', onMessage); + ws.on('error', onError); + ws.on('close', onClose); + this.socketCount++; + this.clients.push(ws); + + // Minion detection + if (this.config.serverMinionThreshold) { + if ((ws.lastAliveTime - this.startTime) / 1000 >= this.config.serverMinionIgnoreTime) { + if (this.minionTest.length >= this.config.serverMinionThreshold) { + ws.playerTracker.isMinion = true; + for (var i = 0; i < this.minionTest.length; i++) { + var playerTracker = this.minionTest[i]; + if (!playerTracker.socket.isConnected) continue; + playerTracker.isMinion = true; + } + if (this.minionTest.length) { + this.minionTest.splice(0, 1); + } + } + this.minionTest.push(ws.playerTracker); + } + } +}; + +GameServer.prototype.onClientSocketClose = function (ws, code) { + if (ws._socket.destroy != null && typeof ws._socket.destroy == 'function') { + ws._socket.destroy(); + } + if (this.socketCount < 1) { + Logger.error("GameServer.onClientSocketClose: socketCount=" + this.socketCount); + } else { + this.socketCount--; + } + ws.isConnected = false; + ws.sendPacket = function (data) { }; + ws.closeReason = { code: ws._closeCode, message: ws._closeMessage }; + ws.closeTime = Date.now(); + Logger.write("DISCONNECTED " + ws.remoteAddress + ":" + ws.remotePort + ", code: " + ws._closeCode + ", reason: \"" + ws._closeMessage + "\", name: \"" + ws.playerTracker.getName() + "\""); + + // disconnected effect + var color = this.getGrayColor(ws.playerTracker.getColor()); + ws.playerTracker.setColor(color); + ws.playerTracker.setSkin(""); + ws.playerTracker.cells.forEach(function (cell) { + cell.setColor(color); + }, this); +}; + +GameServer.prototype.onClientSocketError = function (ws, error) { + ws.sendPacket = function (data) { }; +}; + +GameServer.prototype.onClientSocketMessage = function (ws, message) { + if (message.length == 0) { + return; + } + if (message.length > 256) { + ws.close(1009, "Spam"); + return; + } + ws.packetHandler.handleMessage(message); +}; + +GameServer.prototype.setBorder = function (width, height) { + var hw = width / 2; + var hh = height / 2; + this.border = { + minx: -hw, + miny: -hh, + maxx: hw, + maxy: hh, + width: width, + height: height, + centerx: 0, + centery: 0 + }; +}; + +GameServer.prototype.getTick = function () { + return this.tickCounter; +}; + +GameServer.prototype.getMode = function () { + return this.gameMode; +}; + +GameServer.prototype.getNextNodeId = function () { + // Resets integer + if (this.lastNodeId > 2147483647) { + this.lastNodeId = 1; + } + return this.lastNodeId++ >>> 0; +}; + +GameServer.prototype.getNewPlayerID = function () { + // Resets integer + if (this.lastPlayerId > 2147483647) { + this.lastPlayerId = 1; + } + return this.lastPlayerId++ >>> 0; +}; + +GameServer.prototype.getRandomPosition = function () { + return { + x: Math.floor(this.border.minx + this.border.width * Math.random()), + y: Math.floor(this.border.miny + this.border.height * Math.random()) + }; +}; + +GameServer.prototype.getGrayColor = function (rgb) { + var luminance = Math.min(255, (rgb.r * 0.2125 + rgb.g * 0.7154 + rgb.b * 0.0721)) >>> 0; + return { + r: luminance, + g: luminance, + b: luminance + }; +}; + +GameServer.prototype.getRandomColor = function () { + var h = 360 * Math.random(); + var s = 248 / 255; + var v = 1; + + // hsv to rgb + var rgb = { r: v, g: v, b: v }; // achromatic (grey) + if (s > 0) { + h /= 60; // sector 0 to 5 + var i = Math.floor(h) >> 0; + var f = h - i; // factorial part of h + var p = v * (1 - s); + var q = v * (1 - s * f); + var t = v * (1 - s * (1 - f)); + switch (i) { + case 0: rgb = { r: v, g: t, b: p }; break + case 1: rgb = { r: q, g: v, b: p }; break + case 2: rgb = { r: p, g: v, b: t }; break + case 3: rgb = { r: p, g: q, b: v }; break + case 4: rgb = { r: t, g: p, b: v }; break + default: rgb = { r: v, g: p, b: q }; break + } + } + // check color range + rgb.r = Math.max(rgb.r, 0); + rgb.g = Math.max(rgb.g, 0); + rgb.b = Math.max(rgb.b, 0); + rgb.r = Math.min(rgb.r, 1); + rgb.g = Math.min(rgb.g, 1); + rgb.b = Math.min(rgb.b, 1); + return { + r: (rgb.r * 255) >>> 0, + g: (rgb.g * 255) >>> 0, + b: (rgb.b * 255) >>> 0 + }; +}; + +GameServer.prototype.updateNodeQuad = function (node) { + var item = node.quadItem; + if (item == null) { + throw new TypeError("GameServer.updateNodeQuad: quadItem is null!"); + } + var x = node.position.x; + var y = node.position.y; + var size = node.getSize(); + // check for change + if (item.x === x && item.y === y && item.size === size) { + return; + } + // update quad tree + item.x = x; + item.y = y; + item.size = size; + item.bound.minx = x - size; + item.bound.miny = y - size; + item.bound.maxx = x + size; + item.bound.maxy = y + size; + this.quadTree.update(item); +}; + + +GameServer.prototype.addNode = function (node) { + var x = node.position.x; + var y = node.position.y; + var size = node.getSize(); + node.quadItem = { + cell: node, + x: x, + y: y, + size: size, + bound: { minx: x-size, miny: y-size, maxx: x+size, maxy: y+size } + }; + this.quadTree.insert(node.quadItem); + + this.nodes.push(node); + + // Adds to the owning player's screen + if (node.owner) { + node.setColor(node.owner.getColor()); + node.owner.cells.push(node); + node.owner.socket.sendPacket(new Packet.AddNode(node.owner, node)); + } + + // Special on-add actions + node.onAdd(this); +}; + +GameServer.prototype.removeNode = function (node) { + if (node.quadItem == null) { + throw new TypeError("GameServer.removeNode: attempt to remove invalid node!"); + } + node.isRemoved = true; + this.quadTree.remove(node.quadItem); + node.quadItem = null; + + // Remove from main nodes list + var index = this.nodes.indexOf(node); + if (index != -1) { + this.nodes.splice(index, 1); + } + + // Remove from moving cells list + index = this.movingNodes.indexOf(node); + if (index != -1) { + this.movingNodes.splice(index, 1); + } + + // Special on-remove actions + node.onRemove(this); +}; + +GameServer.prototype.updateClients = function () { + // check minions + for (var i = 0; i < this.minionTest.length; ) { + var playerTracker = this.minionTest[i]; + if (this.stepDateTime - playerTracker.connectedTime > this.config.serverMinionInterval) { + this.minionTest.splice(i, 1); + } else { + i++; + } + } + // check dead clients + for (var i = 0; i < this.clients.length; ) { + var playerTracker = this.clients[i].playerTracker; + playerTracker.checkConnection(); + if (playerTracker.isRemoved) { + // remove dead client + this.clients.splice(i, 1); + } else { + i++; + } + } + // update + for (var i = 0; i < this.clients.length; i++) { + this.clients[i].playerTracker.updateTick(); + } + for (var i = 0; i < this.clients.length; i++) { + this.clients[i].playerTracker.sendUpdate(); + } +}; + +GameServer.prototype.updateLeaderboard = function () { + // Update leaderboard with the gamemode's method + this.leaderboard = []; + this.leaderboardType = -1; + this.gameMode.updateLB(this); + + if (!this.gameMode.specByLeaderboard) { + // Get client with largest score if gamemode doesn't have a leaderboard + var clients = this.clients.valueOf(); + + // Use sort function + clients.sort(function (a, b) { + return b.playerTracker.getScore() - a.playerTracker.getScore(); + }); + //this.largestClient = clients[0].playerTracker; + this.largestClient = null; + if (clients[0] != null) + this.largestClient = clients[0].playerTracker; + } else { + this.largestClient = this.gameMode.rankOne; + } +}; + +GameServer.prototype.onChatMessage = function (from, to, message) { + if (message == null) return; + message = message.trim(); + if (message == "") return; + if (from && message.length > 0 && message[0] == '/') { + // player command + message = message.slice(1, message.length); + from.socket.playerCommand.executeCommandLine(message); + return; + } + if (!this.config.serverChat) { + // chat is disabled + return; + } + if (from && from.isMuted) { + // player is muted + return; + } + if (message.length > 64) { + message = message.slice(0, 64); + } + if (this.config.serverChatAscii) { + for (var i = 0; i < message.length; i++) { + var c = message.charCodeAt(i); + if (c < 0x20 || c > 0x7F) { + if (from) { + this.sendChatMessage(null, from, "You can use ASCII text only!"); + } + return; + } + } + } + if (this.checkBadWord(message)) { + if (from) { + this.sendChatMessage(null, from, "Stop insulting others! Keep calm and be friendly please"); + } + return; + } + if (from) { + Logger.writeDebug("[CHAT][" + from.socket.remoteAddress + ":" + from.socket.remotePort + "][" + from.getFriendlyName() + "] " + message); + } else { + Logger.writeDebug("[CHAT][][]: " + message); + } + this.sendChatMessage(from, to, message); +}; + +GameServer.prototype.sendChatMessage = function (from, to, message) { + for (var i = 0; i < this.clients.length; i++) { + var client = this.clients[i]; + if (client == null) continue; + if (to == null || to == client.playerTracker) + client.sendPacket(new Packet.ChatMessage(from, message)); + } +}; + +GameServer.prototype.timerLoop = function () { + var timeStep = 40; + + var ts = Date.now(); + var dt = ts - this.timeStamp; + if (dt < timeStep - 5) { + setTimeout(this.timerLoopBind, ((timeStep - 5) - dt) >> 0); + return; + } + if (dt < timeStep - 1) { + setTimeout(this.timerLoopBind, 0); + return; + } + if (dt < timeStep) { + //process.nextTick(this.timerLoopBind); + setTimeout(this.timerLoopBind, 0); + return; + } + if (dt > 120) { + // too high lag => resynchronize + this.timeStamp = ts-timeStep; + } + // update average + this.updateTimeAvg += 0.5 * (this.updateTime - this.updateTimeAvg); + // calculate next + if (this.timeStamp == 0) + this.timeStamp = ts; + this.timeStamp += timeStep; + //process.nextTick(this.mainLoopBind); + //process.nextTick(this.timerLoopBind); + setTimeout(this.mainLoopBind, 0); + setTimeout(this.timerLoopBind, 0); +}; + +GameServer.prototype.mainLoop = function () { + this.stepDateTime = Date.now(); + var tStart = process.hrtime(); + + // Loop main functions + if (this.run) { + this.updateMoveEngine(); + if ((this.getTick() % this.config.spawnInterval) == 0) { + this.updateFood(); // Spawn food + this.updateVirus(); // Spawn viruses + } + this.gameMode.onTick(this); + if (((this.getTick() + 3) % (1000 / 40)) == 0) { + // once per second + this.updateMassDecay(); + } + } + + this.updateClients(); + + if (((this.getTick() + 7) % (1000 / 40)) == 0) { + // once per second + this.updateLeaderboard(); + } + + // ping server tracker + if (this.config.serverTracker && (this.getTick() % (10000 / 40)) == 0) { + // once per 30 seconds + this.pingServerTracker(); + } + + if (this.run) { + this.tickCounter++; + } + var tEnd = process.hrtime(tStart); + this.updateTime = tEnd[0] * 1000 + tEnd[1] / 1000000; +}; + +GameServer.prototype.startingFood = function () { + // Spawns the starting amount of food cells + for (var i = 0; i < this.config.foodMinAmount; i++) { + this.spawnFood(); + } +}; + +GameServer.prototype.updateFood = function () { + var maxCount = this.config.foodMinAmount - this.currentFood; + var spawnCount = Math.min(maxCount, this.config.foodSpawnAmount); + for (var i = 0; i < spawnCount; i++) { + this.spawnFood(); + } +}; + +GameServer.prototype.updateVirus = function () { + var maxCount = this.config.virusMinAmount - this.nodesVirus.length; + var spawnCount = Math.min(maxCount, 2); + for (var i = 0; i < spawnCount; i++) { + this.spawnVirus(); + } +}; + +GameServer.prototype.spawnFood = function () { + var cell = new Entity.Food(this, null, this.getRandomPosition(), this.config.foodMinSize); + if (this.config.foodMassGrow) { + var size = cell.getSize(); + var maxGrow = this.config.foodMaxSize - size; + size += maxGrow * Math.random(); + cell.setSize(size); + } + cell.setColor(this.getRandomColor()); + this.addNode(cell); +}; + +GameServer.prototype.spawnVirus = function () { + // Spawns a virus + var pos = this.getRandomPosition(); + if (this.willCollide(pos, this.config.virusMinSize)) { + // cannot find safe position => do not spawn + return; + } + var v = new Entity.Virus(this, null, pos, this.config.virusMinSize); + this.addNode(v); +}; + +GameServer.prototype.spawnPlayer = function (player, pos, size) { + // Check if can spawn from ejected mass + if (!pos && this.config.ejectSpawnPlayer && this.nodesEjected.length > 0) { + if (Math.random() >= 0.5) { + // Spawn from ejected mass + var index = (this.nodesEjected.length - 1) * Math.random() >>> 0; + var eject = this.nodesEjected[index]; + if (!eject.isRemoved) { + this.removeNode(eject); + pos = { + x: eject.position.x, + y: eject.position.y + }; + if (!size) { + size = Math.max(eject.getSize(), this.config.playerStartSize); + } + } + } + } + if (pos == null) { + // Get random pos + var pos = this.getRandomPosition(); + // 10 attempts to find safe position + for (var i = 0; i < 10 && this.willCollide(pos, this.config.playerMinSize); i++) { + pos = this.getRandomPosition(); + } + } + if (size == null) { + // Get starting mass + size = this.config.playerStartSize; + } + + // Spawn player and add to world + var cell = new Entity.PlayerCell(this, player, pos, size); + this.addNode(cell); + + // Set initial mouse coords + player.mouse = { + x: pos.x, + y: pos.y + }; +}; + +GameServer.prototype.willCollide = function (pos, size) { + // Look if there will be any collision with the current nodes + var bound = { + minx: pos.x - size, + miny: pos.y - size, + maxx: pos.x + size, + maxy: pos.y + size + }; + return this.quadTree.any( + bound, + function (item) { + return item.cell.cellType == 0; // check players only + }); +}; + +// Checks cells for collision. +// Returns collision manifold or null if there is no collision +GameServer.prototype.checkCellCollision = function (cell, check) { + var r = cell.getSize() + check.getSize(); + var dx = check.position.x - cell.position.x; + var dy = check.position.y - cell.position.y; + var squared = dx * dx + dy * dy; // squared distance from cell to check + if (squared > r * r) { + // no collision + return null; + } + // create collision manifold + return { + cell1: cell, + cell2: check, + r: r, // radius sum + dx: dx, // delta x from cell1 to cell2 + dy: dy, // delta y from cell1 to cell2 + squared: squared // squared distance from cell1 to cell2 + }; +}; + +// Resolves rigid body collision +GameServer.prototype.resolveRigidCollision = function (manifold, border) { + // distance from cell1 to cell2 + var d = Math.sqrt(manifold.squared); + if (d <= 0) return; + var invd = 1 / d; + + // normal + var nx = ~~manifold.dx * invd; + var ny = ~~manifold.dy * invd; + + // body penetration distance + var penetration = manifold.r - d; + if (penetration <= 0) return; + + // penetration vector = penetration * normal + var px = penetration * nx; + var py = penetration * ny; + + // body impulse + var totalMass = manifold.cell1.getSizeSquared() + manifold.cell2.getSizeSquared(); + if (totalMass <= 0) return; + var invTotalMass = 1 / totalMass; + var impulse1 = manifold.cell2.getSizeSquared() * invTotalMass; + var impulse2 = manifold.cell1.getSizeSquared() * invTotalMass; + + // apply extrusion force + manifold.cell1.position.x -= px * impulse1; + manifold.cell1.position.y -= py * impulse1; + manifold.cell2.position.x += px * impulse2; + manifold.cell2.position.y += py * impulse2; + // clip to border bounds + manifold.cell1.checkBorder(border); + manifold.cell2.checkBorder(border); +}; + +// Checks if collision is rigid body collision +GameServer.prototype.checkRigidCollision = function (manifold) { + if (!manifold.cell1.owner || !manifold.cell2.owner) + return false; + if (manifold.cell1.owner != manifold.cell2.owner) { + // Different owners + return this.gameMode.haveTeams && + manifold.cell1.owner.getTeam() == manifold.cell2.owner.getTeam(); + } + // The same owner + if (manifold.cell1.owner.mergeOverride) + return false; + var tick = this.getTick(); + if (manifold.cell1.getAge(tick) < 15 || manifold.cell2.getAge(tick) < 15) { + // just splited => ignore + return false; + } + return !manifold.cell1.canRemerge() || !manifold.cell2.canRemerge(); +}; + +// Resolves non-rigid body collision +GameServer.prototype.resolveCollision = function (manifold) { + var minCell = manifold.cell1; + var maxCell = manifold.cell2; + // check if any cell already eaten + if (minCell.isRemoved || maxCell.isRemoved) + return; + if (minCell.getSize() > maxCell.getSize()) { + minCell = manifold.cell2; + maxCell = manifold.cell1; + } + + // check distance + var eatDistance = maxCell.getSize() - minCell.getSize() / 3; + if (manifold.squared >= eatDistance * eatDistance) { + // too far => can't eat + return; + } + + if (minCell.owner && minCell.owner == maxCell.owner) { + // collision owned/owned => ignore or resolve or remerge + + var tick = this.getTick(); + if (minCell.getAge(tick) < 15 || maxCell.getAge(tick) < 15) { + // just splited => ignore + return; + } + if (!minCell.owner.mergeOverride) { + // not force remerge => check if can remerge + if (!minCell.canRemerge() || !maxCell.canRemerge()) { + // cannot remerge + return; + } + } + } else { + // collision owned/enemy => check if can eat + + // Team check + if (this.gameMode.haveTeams && minCell.owner && maxCell.owner) { + if (minCell.owner.getTeam() == maxCell.owner.getTeam()) { + // cannot eat team member + return; + } + } + // Size check + if (maxCell.getSize() <= minCell.getSize() * 1.15) { + // too large => can't eat + return; + } + } + if (!maxCell.canEat(minCell)) { + // maxCell don't want to eat + return; + } + + // Now maxCell can eat minCell + minCell.isRemoved = true; + + // Disable mergeOverride on the last merging cell + // We need to disable it before onCosume to prevent merging loop + // (onConsume may cause split for big mass) + if (minCell.owner && minCell.owner.cells.length <= 2) { + minCell.owner.mergeOverride = false; + } + + var isMinion = (maxCell.owner && maxCell.owner.isMinion) || + (minCell.owner && minCell.owner.isMinion); + if (!isMinion) { + // Consume effect + maxCell.onEat(minCell); + minCell.onEaten(maxCell); + + // update bounds + this.updateNodeQuad(maxCell); + } + + // Remove cell + minCell.setKiller(maxCell); + this.removeNode(minCell); +}; + +GameServer.prototype.updateMoveEngine = function () { + var tick = this.getTick(); + // Move player cells + for (var i in this.clients) { + var client = this.clients[i].playerTracker; + var checkSize = !client.mergeOverride || client.cells.length == 1; + for (var j = 0; j < client.cells.length; j++) { + var cell1 = client.cells[j]; + if (cell1.isRemoved) + continue; + cell1.updateRemerge(this); + cell1.moveUser(this.border); + cell1.move(this.border); + + // check size limit + if (checkSize && cell1.getSize() > this.config.playerMaxSize && cell1.getAge(tick) >= 15) { + if (client.cells.length >= this.config.playerMaxCells) { + // cannot split => just limit + cell1.setSize(this.config.playerMaxSize); + } else { + // split + var maxSplit = this.config.playerMaxCells - client.cells.length; + var maxMass = this.config.playerMaxSize * this.config.playerMaxSize; + var count = (cell1.getSizeSquared() / maxMass) >> 0; + var count = Math.min(count, maxSplit); + var splitSize = cell1.getSize() / Math.sqrt(count + 1); + var splitMass = splitSize * splitSize / 100; + var angle = Math.random() * 2 * Math.PI; + var step = 2 * Math.PI / count; + for (var k = 0; k < count; k++) { + this.splitPlayerCell(client, cell1, angle, splitMass); + angle += step; + } + } + } + this.updateNodeQuad(cell1); + } + } + // Move moving cells + for (var i = 0; i < this.movingNodes.length; ) { + var cell1 = this.movingNodes[i]; + if (cell1.isRemoved) + continue; + cell1.move(this.border); + this.updateNodeQuad(cell1); + if (!cell1.isMoving) + this.movingNodes.splice(i, 1); + else + i++; + } + + // === check for collisions === + + // Scan for player cells collisions + var self = this; + var rigidCollisions = []; + var eatCollisions = []; + for (var i in this.clients) { + var client = this.clients[i].playerTracker; + for (var j = 0; j < client.cells.length; j++) { + var cell1 = client.cells[j]; + if (cell1 == null) continue; + this.quadTree.find(cell1.quadItem.bound, function (item) { + var cell2 = item.cell; + if (cell2 == cell1) return; + var manifold = self.checkCellCollision(cell1, cell2); + if (manifold == null) return; + if (self.checkRigidCollision(manifold)) + rigidCollisions.push({ cell1: cell1, cell2: cell2 }); + else + eatCollisions.push({ cell1: cell1, cell2: cell2 }); + }); + } + } + + // resolve rigid body collisions + ////for (var z = 0; z < 2; z++) { // loop for better rigid body resolution quality (slow) + for (var k = 0; k < rigidCollisions.length; k++) { + var c = rigidCollisions[k]; + var manifold = this.checkCellCollision(c.cell1, c.cell2); + if (manifold == null) continue; + this.resolveRigidCollision(manifold, this.border); + } + ////} + // Update quad tree + for (var k = 0; k < rigidCollisions.length; k++) { + var c = rigidCollisions[k]; + this.updateNodeQuad(c.cell1); + this.updateNodeQuad(c.cell2); + } + rigidCollisions = null; + + // resolve eat collisions + for (var k = 0; k < eatCollisions.length; k++) { + var c = eatCollisions[k]; + var manifold = this.checkCellCollision(c.cell1, c.cell2); + if (manifold == null) continue; + this.resolveCollision(manifold); + } + eatCollisions = null; + + //this.gameMode.onCellMove(cell1, this); + + // Scan for ejected cell collisions (scan for ejected or virus only) + rigidCollisions = []; + eatCollisions = []; + var self = this; + for (var i = 0; i < this.movingNodes.length; i++) { + var cell1 = this.movingNodes[i]; + if (cell1.isRemoved) continue; + this.quadTree.find(cell1.quadItem.bound, function (item) { + var cell2 = item.cell; + if (cell2 == cell1) + return; + var manifold = self.checkCellCollision(cell1, cell2); + if (manifold == null) return; + if (cell1.cellType == 3 && cell2.cellType == 3) { + // ejected/ejected + rigidCollisions.push({ cell1: cell1, cell2: cell2 }); + // add to moving nodes if needed + if (!cell1.isMoving) { + cell1.isMoving = true + self.movingNodes.push(cell1); + } + if (!cell2.isMoving) { + cell2.isMoving = true + self.movingNodes.push(cell2); + } + } + else { + eatCollisions.push({ cell1: cell1, cell2: cell2 }); + } + }); + } + + // resolve rigid body collisions + for (var k = 0; k < rigidCollisions.length; k++) { + var c = rigidCollisions[k]; + var manifold = this.checkCellCollision(c.cell1, c.cell2); + if (manifold == null) continue; + this.resolveRigidCollision(manifold, this.border); + // position changed! don't forgot to update quad-tree + } + // Update quad tree + for (var k = 0; k < rigidCollisions.length; k++) { + var c = rigidCollisions[k]; + this.updateNodeQuad(c.cell1); + this.updateNodeQuad(c.cell2); + } + rigidCollisions = null; + + // resolve eat collisions + for (var k = 0; k < eatCollisions.length; k++) { + var c = eatCollisions[k]; + var manifold = this.checkCellCollision(c.cell1, c.cell2); + if (manifold == null) continue; + this.resolveCollision(manifold); + } +}; + +// Returns masses in descending order +GameServer.prototype.splitMass = function (mass, count) { + // min throw size (vanilla 44) + var throwSize = this.config.playerMinSize + 12; + var throwMass = throwSize * throwSize / 100; + + // check maxCount + var maxCount = count; + var curMass = mass; + while (maxCount > 1 && curMass / (maxCount - 1) < throwMass) { + maxCount = maxCount / 2 >>> 0; + } + if (maxCount < 2) { + return [mass]; + } + + // calculate mass + var minMass = this.config.playerMinSize * this.config.playerMinSize / 100; + var splitMass = curMass / maxCount; + if (splitMass < minMass) { + return [mass]; + } + var masses = []; + if (maxCount < 3 || maxCount < count || curMass / throwMass <= 30) { + // Monotone blow up + for (var i = 0; i < maxCount; i++) { + masses.push(splitMass); + } + } else { + // Diverse blow up + // Barbosik: draft version + var restCount = maxCount; + while (restCount > 2) { + var splitMass = curMass / 2; + if (splitMass <= throwMass) { + break; + } + var max = curMass - throwMass * (restCount - 1); + if (max <= throwMass || splitMass >= max) { + break; + } + masses.push(splitMass); + curMass -= splitMass; + restCount--; + } + var splitMass = curMass / 4; + if (splitMass > throwMass) { + while (restCount > 2) { + var max = curMass - throwMass * (restCount - 1); + if (max <= throwMass || splitMass >= max) { + break; + } + masses.push(splitMass); + curMass -= splitMass; + restCount--; + } + } + var splitMass = curMass / 8; + if (splitMass > throwMass) { + while (restCount > 2) { + var max = curMass - throwMass * (restCount - 1); + if (max <= throwMass || splitMass >= max) { + break; + } + masses.push(splitMass); + curMass -= splitMass; + restCount--; + } + } + if (restCount > 1) { + splitMass = curMass - throwMass * (restCount - 1); + if (splitMass > throwMass) { + masses.push(splitMass); + curMass -= splitMass; + restCount--; + } + } + if (restCount > 0) { + splitMass = curMass / restCount; + if (splitMass < throwMass - 0.001) { + Logger.warn("GameServer.splitMass: throwMass-splitMass = " + (throwMass - splitMass).toFixed(3) + " (" + mass.toFixed(4) + ", " + count + ")"); + } + while (restCount > 0) { + masses.push(splitMass); + restCount--; + } + } + } + //Logger.debug("===GameServer.splitMass==="); + //Logger.debug("mass = " + mass.toFixed(3) + " | " + Math.sqrt(mass * 100).toFixed(3)); + //var sum = 0; + //for (var i = 0; i < masses.length; i++) { + // Logger.debug("mass[" + i + "] = " + masses[i].toFixed(3) + " | " + Math.sqrt(masses[i] * 100).toFixed(3)); + // sum += masses[i] + //} + //Logger.debug("sum = " + sum.toFixed(3) + " | " + Math.sqrt(sum * 100).toFixed(3)); + return masses; +}; + +GameServer.prototype.splitCells = function (client) { + // it seems that vanilla uses order by cell age + var cellToSplit = []; + for (var i = 0; i < client.cells.length; i++) { + var cell = client.cells[i]; + if (cell.getSize() < this.config.playerMinSplitSize) { + continue; + } + cellToSplit.push(cell); + if (cellToSplit.length + client.cells.length >= this.config.playerMaxCells) + break; + } + var splitCells = 0; // How many cells have been split + for (var i = 0; i < cellToSplit.length; i++) { + var cell = cellToSplit[i]; + var dx = ~~(client.mouse.x - cell.position.x); + var dy = ~~(client.mouse.y - cell.position.y); + var dl = dx * dx + dy * dy; + if (dl < 1) { + dx = 1; + dy = 0; + } + var angle = Math.atan2(dx, dy); + if (isNaN(angle)) angle = Math.PI / 2; + + if (this.splitPlayerCell(client, cell, angle, null)) { + splitCells++; + } + } +}; + +// TODO: replace mass with size (Virus) +GameServer.prototype.splitPlayerCell = function (client, parent, angle, mass) { + // Returns boolean whether a cell has been split or not. You can use this in the future. + + if (client.cells.length >= this.config.playerMaxCells) { + // Player cell limit + return false; + } + + var size1 = 0; + var size2 = 0; + if (mass == null) { + size1 = parent.getSplitSize(); + size2 = size1; + } else { + size2 = Math.sqrt(mass * 100); + size1 = Math.sqrt(parent.getSize() * parent.getSize() - size2 * size2); + } + if (isNaN(size1) || size1 < this.config.playerMinSize) { + return false; + } + + // Remove mass from parent cell first + parent.setSize(size1); + + // make a small shift to the cell position to prevent extrusion in wrong direction + var pos = { + x: parent.position.x + 40 * Math.sin(angle), + y: parent.position.y + 40 * Math.cos(angle) + }; + + // Create cell + var newCell = new Entity.PlayerCell(this, client, pos, size2); + newCell.setBoost(780, angle); + + // Add to node list + this.addNode(newCell); + return true; +}; + +GameServer.prototype.canEjectMass = function (client) { + var tick = this.getTick(); + if (client.lastEject == null) { + // first eject + client.lastEject = tick; + return true; + } + var dt = tick - client.lastEject; + if (dt < this.config.ejectCooldown) { + // reject (cooldown) + return false; + } + client.lastEject = tick; + return true; +}; + +GameServer.prototype.ejectMass = function (client) { + if (!this.canEjectMass(client)) + return; + for (var i = 0; i < client.cells.length; i++) { + var cell = client.cells[i]; + + if (!cell) { + continue; + } + + if (cell.getSize() < this.config.playerMinSplitSize) { + continue; + } + var size2 = this.config.ejectSize; + var sizeLoss = this.config.ejectSizeLoss; + var sizeSquared = cell.getSizeSquared() - sizeLoss * sizeLoss; + if (sizeSquared < this.config.playerMinSize * this.config.playerMinSize) { + continue; + } + var size1 = Math.sqrt(sizeSquared); + + var dx = client.mouse.x - cell.position.x; + var dy = client.mouse.y - cell.position.y; + var dl = dx * dx + dy * dy; + if (dl < 1) { + dx = 1; + dy = 0; + } else { + dl = Math.sqrt(dl); + dx /= dl; + dy /= dl; + } + + // Remove mass from parent cell first + cell.setSize(size1); + + // Get starting position + var pos = { + x: cell.position.x + dx * cell.getSize(), + y: cell.position.y + dy * cell.getSize() + }; + + var angle = Math.atan2(dx, dy); + if (isNaN(angle)) angle = Math.PI / 2; + + // Randomize angle + angle += (Math.random() * 0.6) - 0.3; + + // Create cell + var ejected = new Entity.EjectedMass(this, null, pos, size2); + ejected.ejector = cell; + ejected.setColor(cell.getColor()); + ejected.setBoost(this.config.ejectDistance, angle); + + this.addNode(ejected); + } +}; + +GameServer.prototype.shootVirus = function (parent, angle) { + var parentPos = { + x: parent.position.x, + y: parent.position.y, + }; + + var newVirus = new Entity.Virus(this, null, parentPos, this.config.virusMinSize); + newVirus.setBoost(780, angle); + + // Add to moving cells list + this.addNode(newVirus); +}; + +GameServer.prototype.getNearestVirus = function (cell) { + // Loop through all viruses on the map. There is probably a more efficient way of doing this but whatever + for (var i = 0; i < this.nodesVirus.length; i++) { + var check = this.nodesVirus[i]; + if (check === null) continue; + if (this.checkCellCollision(cell, check) != null) { + return check; + } + } +}; + +GameServer.prototype.updateMassDecay = function () { + if (!this.config.playerDecayRate) { + return; + } + var decay = 1 - this.config.playerDecayRate * this.gameMode.decayMod; + // Loop through all player cells + for (var i = 0; i < this.clients.length; i++) { + var playerTracker = this.clients[i].playerTracker; + for (var j = 0; j < playerTracker.cells.length; j++) { + var cell = playerTracker.cells[j]; + var size = cell.getSize(); + if (size <= this.config.playerMinSize) + continue; + var size = Math.sqrt(size * size * decay); + size = Math.max(size, this.config.playerMinSize); + if (size != cell.getSize()) { + cell.setSize(size); + } + } + } +}; + +GameServer.prototype.getPlayerById = function (id) { + if (id == null) return null; + for (var i = 0; i < this.clients.length; i++) { + var playerTracker = this.clients[i].playerTracker; + if (playerTracker.pID == id) { + return playerTracker; + } + } + return null; +}; + +GameServer.prototype.checkSkinName = function (skinName) { + if (!skinName) { + return true; + } + if (skinName.length == 1 || skinName.length > 25) { + return false; + } + if (skinName[0] != '%' /* && skinName[0] != ':' */) { + return false; + } + for (var i = 1; i < skinName.length; i++) { + var c = skinName.charCodeAt(i); + if (c < 0x21 || c > 0x7F || c == '/' || c == '\\' || c == ':' || c == '%' || c == '?' || c == '&' || c == '<' || c == '>') { + return false; + } + } + return true; +}; + +var fileNameConfig = './gameserver.ini'; +var fileNameBadWords = './badwords.txt'; +var fileNameIpBan = './ipbanlist.txt'; +var fileNameUsers = './userRoles.json'; + +GameServer.prototype.loadConfig = function () { + try { + if (!fs.existsSync(fileNameConfig)) { + // No config + Logger.warn("Config not found... Generating new config"); + // Create a new config + fs.writeFileSync(fileNameConfig, ini.stringify(this.config), 'utf-8'); + } else { + // Load the contents of the config file + var load = ini.parse(fs.readFileSync(fileNameConfig, 'utf-8')); + // Replace all the default config's values with the loaded config's values + for (var key in load) { + if (this.config.hasOwnProperty(key)) { + this.config[key] = load[key]; + } else { + Logger.error("Unknown gameserver.ini value: " + key); + } + } + } + } catch (err) { + Logger.error(err.stack); + Logger.error("Failed to load " + fileNameConfig + ": " + err.message); + } + // check config (min player size = 32 => mass = 10.24) + this.config.playerMinSize = Math.max(32, this.config.playerMinSize); + Logger.setVerbosity(this.config.logVerbosity); + Logger.setFileVerbosity(this.config.logFileVerbosity); +}; + +GameServer.prototype.loadBadWords = function () { + try { + if (!fs.existsSync(fileNameBadWords)) { + Logger.warn(fileNameBadWords + " not found"); + } else { + var words = fs.readFileSync(fileNameBadWords, 'utf-8'); + words = words.split(/[\r\n]+/); + words = words.map(function (arg) { return arg.trim().toLowerCase(); }); + words = words.filter(function (arg) { return !!arg; }); + this.badWords = words; + Logger.info(this.badWords.length + " bad words loaded"); + } + } catch (err) { + Logger.error(err.stack); + Logger.error("Failed to load " + fileNameBadWords + ": " + err.message); + } +}; + +GameServer.prototype.checkBadWord = function (value) { + if (!value) return false; + value = value.toLowerCase().trim(); + if (!value) return false; + for (var i = 0; i < this.badWords.length; i++) { + if (value.indexOf(this.badWords[i]) >= 0) { + return true; + } + } + return false; +}; + +GameServer.prototype.changeConfig = function (name, value) { + if (value == null || isNaN(value)) { + Logger.warn("Invalid value: " + value); + return; + } + if (!this.config.hasOwnProperty(name)) { + Logger.warn("Unknown config value: " + name); + return; + } + this.config[name] = value; + + // update/validate + this.config.playerMinSize = Math.max(32, this.config.playerMinSize); + Logger.setVerbosity(this.config.logVerbosity); + Logger.setFileVerbosity(this.config.logFileVerbosity); + + Logger.print("Set " + name + " = " + this.config[name]); +}; + +GameServer.prototype.loadUserList = function () { + try { + this.userList = []; + if (!fs.existsSync(fileNameUsers)) { + Logger.warn(fileNameUsers + " is missing."); + return; + } + var usersJson = fs.readFileSync(fileNameUsers, 'utf-8'); + var list = JSON.parse(usersJson.trim()); + for (var i = 0; i < list.length; ) { + var item = list[i]; + if (!item.hasOwnProperty("ip") || + !item.hasOwnProperty("password") || + !item.hasOwnProperty("role") || + !item.hasOwnProperty("name")) { + list.splice(i, 1); + continue; + } + if (!item.password || !item.password.trim()) { + Logger.warn("User account \"" + item.name + "\" disabled"); + list.splice(i, 1); + continue; + } + if (item.ip) { + item.ip = item.ip.trim(); + } + item.password = item.password.trim(); + if (!UserRoleEnum.hasOwnProperty(item.role)) { + Logger.warn("Unknown user role: " + role); + item.role = UserRoleEnum.USER; + } else { + item.role = UserRoleEnum[item.role]; + } + item.name = (item.name || "").trim(); + i++; + } + this.userList = list; + Logger.info(this.userList.length + " user records loaded."); + } catch (err) { + Logger.error(err.stack); + Logger.error("Failed to load " + fileNameUsers + ": " + err.message); + } +} + +GameServer.prototype.userLogin = function (ip, password) { + if (!password) return null; + password = password.trim(); + if (!password) return null; + for (var i = 0; i < this.userList.length; i++) { + var user = this.userList[i]; + if (user.password != password) + continue; + if (user.ip && user.ip != ip) + continue; + return user; + } + return null; +}; + +GameServer.prototype.loadIpBanList = function () { + try { + if (fs.existsSync(fileNameIpBan)) { + // Load and input the contents of the ipbanlist file + this.ipBanList = fs.readFileSync(fileNameIpBan, "utf8").split(/[\r\n]+/).filter(function (x) { + return x != ''; // filter empty lines + }); + Logger.info(this.ipBanList.length + " IP ban records loaded."); + } else { + Logger.warn(fileNameIpBan + " is missing."); + } + } catch (err) { + Logger.error(err.stack); + Logger.error("Failed to load " + fileNameIpBan + ": " + err.message); + } +}; + +GameServer.prototype.saveIpBanList = function () { + try { + var blFile = fs.createWriteStream(fileNameIpBan); + // Sort the blacklist and write. + this.ipBanList.sort().forEach(function (v) { + blFile.write(v + '\n'); + }); + blFile.end(); + Logger.info(this.ipBanList.length + " IP ban records saved."); + } catch (err) { + Logger.error(err.stack); + Logger.error("Failed to save " + fileNameIpBan + ": " + err.message); + } +}; + +GameServer.prototype.checkIpBan = function (ipAddress) { + if (!this.ipBanList || this.ipBanList.length == 0 || ipAddress == "127.0.0.1") { + return false; + } + if (this.ipBanList.indexOf(ipAddress) >= 0) { + return true; + } + var ipBin = ipAddress.split('.'); + if (ipBin.length != 4) { + // unknown IP format + return false; + } + var subNet2 = ipBin[0] + "." + ipBin[1] + ".*.*"; + if (this.ipBanList.indexOf(subNet2) >= 0) { + return true; + } + var subNet1 = ipBin[0] + "." + ipBin[1] + "." + ipBin[2] + ".*"; + if (this.ipBanList.indexOf(subNet1) >= 0) { + return true; + } + return false; +}; + +GameServer.prototype.banIp = function (ip) { + var ipBin = ip.split('.'); + if (ipBin.length != 4) { + Logger.warn("Invalid IP format: " + ip); + return; + } + if (ipBin[0] == "127") { + Logger.warn("Cannot ban localhost"); + return; + } + if (this.ipBanList.indexOf(ip) >= 0) { + Logger.warn(ip + " is already in the ban list!"); + return; + } + this.ipBanList.push(ip); + if (ipBin[2] == "*" || ipBin[3] == "*") { + Logger.print("The IP sub-net " + ip + " has been banned"); + } else { + Logger.print("The IP " + ip + " has been banned"); + } + this.clients.forEach(function (socket) { + // If already disconnected or the ip does not match + if (socket == null || !socket.isConnected || !this.checkIpBan(socket.remoteAddress)) + return; + + // remove player cells + socket.playerTracker.cells.forEach(function (cell) { + this.removeNode(cell); + }, this); + + // disconnect + socket.close(1000, "Banned from server"); + var name = socket.playerTracker.getFriendlyName(); + Logger.print("Banned: \"" + name + "\" with Player ID " + socket.playerTracker.pID); + this.sendChatMessage(null, null, "Banned \"" + name + "\""); // notify to don't confuse with server bug + }, this); + this.saveIpBanList(); +}; + +GameServer.prototype.unbanIp = function (ip) { + var index = this.ipBanList.indexOf(ip); + if (index < 0) { + Logger.warn("IP " + ip + " is not in the ban list!"); + return; + } + this.ipBanList.splice(index, 1); + Logger.print("Unbanned IP: " + ip); + this.saveIpBanList(); +}; + +// Kick player by ID. Use ID = 0 to kick all players +GameServer.prototype.kickId = function (id) { + var count = 0; + this.clients.forEach(function (socket) { + if (socket.isConnected == false) + return; + if (id != 0 && socket.playerTracker.pID != id) + return; + // remove player cells + socket.playerTracker.cells.forEach(function (cell) { + this.removeNode(cell); + }, this); + // disconnect + socket.close(1000, "Kicked from server"); + var name = socket.playerTracker.getFriendlyName(); + Logger.print("Kicked \"" + name + "\""); + this.sendChatMessage(null, null, "Kicked \"" + name + "\""); // notify to don't confuse with server bug + count++; + }, this); + if (count > 0) + return; + if (id == 0) + Logger.warn("No players to kick!"); + else + Logger.warn("Player with ID " + id + " not found!"); +}; + +// Stats server + +GameServer.prototype.startStatsServer = function (port) { + // Do not start the server if the port is negative + if (port < 1) { + return; + } + + // Create stats + this.stats = "Test"; + this.getStats(); + + // Show stats + this.httpServer = http.createServer(function (req, res) { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.writeHead(200); + res.end(this.stats); + }.bind(this)); + this.httpServer.on('error', function (err) { + Logger.error("Stats Server: " + err.message); + }); + + var getStatsBind = this.getStats.bind(this); + this.httpServer.listen(port, function () { + // Stats server + Logger.info("Started stats server on port " + port); + setInterval(getStatsBind, this.config.serverStatsUpdate * 1000); + }.bind(this)); +}; + +GameServer.prototype.getStats = function () { + // Get server statistics + var totalPlayers = 0; + var alivePlayers = 0; + var spectatePlayers = 0; + for (var i = 0; i < this.clients.length; i++) { + var socket = this.clients[i]; + if (socket == null || !socket.isConnected) + continue; + totalPlayers++; + if (socket.playerTracker.cells.length > 0) + alivePlayers++; + else + spectatePlayers++; + } + var s = { + 'server_name': this.config.serverName, + 'server_chat': this.config.serverChat ? "true" : "false", + 'border_width': this.border.width, + 'border_height': this.border.height, + 'gamemode': this.gameMode.name, + 'max_players': this.config.serverMaxConnections, + 'current_players': totalPlayers, + 'alive': alivePlayers, + 'spectators': spectatePlayers, + 'update_time': this.updateTimeAvg.toFixed(3), + 'uptime': Math.round((this.stepDateTime - this.startTime) / 1000 / 60), + 'start_time': this.startTime + }; + this.stats = JSON.stringify(s); +}; + +// Ping the server tracker. +// To list us on the server tracker located at http://ogar.mivabe.nl/master +// Should be called every 30 seconds +GameServer.prototype.pingServerTracker = function () { + // Get server statistics + var totalPlayers = 0; + var alivePlayers = 0; + var spectatePlayers = 0; + var robotPlayers = 0; + for (var i = 0; i < this.clients.length; i++) { + var socket = this.clients[i]; + if (socket == null || socket.isConnected === false) + continue; + if (socket.isConnected == null) { + robotPlayers++; + } + else { + totalPlayers++; + if (socket.playerTracker.cells.length > 0) + alivePlayers++; + else + spectatePlayers++; + } + } + + // Send Ping... + + // ogar-tracker.tk + var obj = { + port: this.config.serverPort, // [mandatory] web socket port which listens for game client connections + name: this.config.serverName, // [mandatory] server name + mode: this.gameMode.name, // [mandatory] game mode + total: totalPlayers, // [mandatory] total online players (server bots is not included!) + alive: alivePlayers, // [mandatory] alive players (server bots is not included!) + spect: spectatePlayers, // [mandatory] spectate players (server bots is not included!) + robot: robotPlayers, // [mandatory] server bots + limit: this.config.serverMaxConnections, // [mandatory] maximum allowed connection count + protocol: 'M', // [mandatory] required protocol id or 'M' for multiprotocol (if all protocols is supported) + uptime: process.uptime() >>> 0, // [mandatory] server uptime [seconds] + w: this.border.width >>> 0, // [mandatory] map border width [integer] + h: this.border.height >>> 0, // [mandatory] map border height [integer] + version: 'MultiOgar ' + pjson.version, // [optional] server version + stpavg: this.updateTimeAvg >>> 0, // [optional] average server loop time + chat: this.config.serverChat ? 1 : 0, // [optional] 0 - chat disabled, 1 - chat enabled + os: os.platform() // [optional] operating system + }; + trackerRequest({ + host: 'ogar-tracker.tk', + port: 80, + path: '/api/ping', + method: 'PUT' + }, 'application/json', JSON.stringify(obj)); + + + // mivabe.nl + // Why don't just to use JSON? + var data = 'current_players=' + totalPlayers + + '&alive=' + alivePlayers + + '&spectators=' + spectatePlayers + + '&max_players=' + this.config.serverMaxConnections + + '&sport=' + this.config.serverPort + + '&gamemode=[*] ' + this.gameMode.name + // we add [*] to indicate that this is multi-server + '&agario=true' + // protocol version + '&name=Unnamed Server' + // we cannot use it, because other value will be used as dns name + '&opp=' + os.platform() + ' ' + os.arch() + // "win32 x64" + '&uptime=' + process.uptime() + // Number of seconds server has been running + '&version=MultiOgar ' + pjson.version + + '&start_time=' + this.startTime; + trackerRequest({ + host: 'ogar.mivabe.nl', + port: 80, + path: '/master', + method: 'POST' + }, 'application/x-www-form-urlencoded', data); + + // c0nsume.me + trackerRequest({ + host: 'c0nsume.me', + port: 80, + path: '/tracker.php', + method: 'POST' + }, 'application/x-www-form-urlencoded', data); +}; + +function trackerRequest(options, type, body) { + if (options.headers == null) + options.headers = {}; + options.headers['user-agent'] = 'MultiOgar' + pjson.version; + options.headers['content-type'] = type; + options.headers['content-length'] = body == null ? 0 : Buffer.byteLength(body, 'utf8'); + var req = http.request(options, function (res) { + if (res.statusCode != 200) { + Logger.writeError("[Tracker][" + options.host + "]: statusCode = " + res.statusCode); + return; + } + res.setEncoding('utf8'); + }); + req.on('error', function (err) { + Logger.writeError("[Tracker][" + options.host + "]: " + err); + }); + req.shouldKeepAlive = false; + req.on('close', function () { + req.destroy(); + }); + req.write(body); + req.end() +}; \ No newline at end of file diff --git a/src/HttpsServer.js b/src/HttpsServer.js new file mode 100644 index 000000000..3248af2a5 --- /dev/null +++ b/src/HttpsServer.js @@ -0,0 +1,101 @@ +var http = require('http'), + https = require('https'), + inherits = require('util').inherits, + httpSocketHandler = http._connectionListener; +var Logger = require('./modules/Logger'); + +var isOldNode = /^v0\.10\./.test(process.version); + +function Server(tlsconfig, requestListener) { + if (!(this instanceof Server)) + return new Server(tlsconfig, requestListener); + + if (typeof tlsconfig === 'function') { + requestListener = tlsconfig; + tlsconfig = undefined; + } + + if (typeof tlsconfig === 'object') { + this.removeAllListeners('connection'); + + https.Server.call(this, tlsconfig, requestListener); + + // capture https socket handler, it's not exported like http's socket + // handler + var connev = this._events.connection; + if (typeof connev === 'function') + this._tlsHandler = connev; + else + this._tlsHandler = connev[connev.length - 1]; + this.removeListener('connection', this._tlsHandler); + + this._connListener = connectionListener; + this.on('connection', connectionListener); + + // copy from http.Server + this.timeout = 2 * 60 * 1000; + this.allowHalfOpen = false; + this.httpAllowHalfOpen = false; + } else + http.Server.call(this, requestListener); +} +inherits(Server, https.Server); + +Server.prototype.setTimeout = function (msecs, callback) { + this.timeout = msecs; + if (callback) + this.on('timeout', callback); +}; + +Server.prototype.__httpSocketHandler = httpSocketHandler; + +var connectionListener; +if (isOldNode) { + connectionListener = function (socket) { + var logip = socket.remoteAddress + ":" + socket.remotePort; + socket.on('error', function (err) { + Logger.writeError("[" + logip + "] " + err.stack); + }); + var self = this; + socket.ondata = function (d, start, end) { + var firstByte = d[start]; + if (firstByte < 32 || firstByte >= 127) { + // tls/ssl + socket.ondata = null; + self._tlsHandler(socket); + socket.push(d.slice(start, end)); + } else { + self.__httpSocketHandler(socket); + socket.ondata(d, start, end); + } + }; + }; +} else { + connectionListener = function (socket) { + var logip = socket.remoteAddress + ":" + socket.remotePort; + socket.on('error', function (err) { + Logger.writeError("[" + logip + "] " + err.stack); + }); + var self = this; + var data = socket.read(1); + if (data === null) { + socket.once('readable', function () { + self._connListener(socket); + }); + } else { + var firstByte = data[0]; + socket.unshift(data); + if (firstByte < 32 || firstByte >= 127) { + // tls/ssl + this._tlsHandler(socket); + } else + this.__httpSocketHandler(socket); + } + }; +} + +exports.Server = Server; + +exports.createServer = function (tlsconfig, requestListener) { + return new Server(tlsconfig, requestListener); +}; \ No newline at end of file diff --git a/src/PacketHandler.js b/src/PacketHandler.js index 17301a31f..3a3989895 100644 --- a/src/PacketHandler.js +++ b/src/PacketHandler.js @@ -1,129 +1,262 @@ -var Packet = require('./packet'); - -function PacketHandler(gameServer, socket) { - this.gameServer = gameServer; - this.socket = socket; - // Detect protocol version - we can do something about it later - this.protocolVersion = 0; - - this.pressQ = false; - this.pressW = false; - this.pressSpace = false; -} - -module.exports = PacketHandler; - -PacketHandler.prototype.handleMessage = function(message) { - function stobuf(buf) { - var length = buf.length; - var arrayBuf = new ArrayBuffer(length); - var view = new Uint8Array(arrayBuf); - - for (var i = 0; i < length; i++) { - view[i] = buf[i]; - } - - return view.buffer; - } - - // Discard empty messages - if (message.length == 0) { - return; - } - - var buffer = stobuf(message); - var view = new DataView(buffer); - var packetId = view.getUint8(0, true); - - switch (packetId) { - case 0: - // Set Nickname - if (this.protocolVersion == 5) { - // Check for invalid packets - if ((view.byteLength + 1) % 2 == 1) { - break; - } - var nick = ""; - var maxLen = this.gameServer.config.playerMaxNickLength * 2; // 2 bytes per char - for (var i = 1; i < view.byteLength && i <= maxLen; i += 2) { - var charCode = view.getUint16(i, true); - if (charCode == 0) { - break; - } - - nick += String.fromCharCode(charCode); - } - this.setNickname(nick); - } else { - var name = message.slice(1, message.length - 1).toString().substr(0, this.gameServer.config.playerMaxNickLength); - this.setNickname(name); - } - break; - case 1: - // Spectate mode - if (this.socket.playerTracker.cells.length <= 0) { - // Make sure client has no cells - this.socket.playerTracker.spectate = true; - } - break; - case 16: - // Set Target - if (view.byteLength == 13) { - var client = this.socket.playerTracker; - client.mouse.x = view.getInt32(1, true) - client.scrambleX; - client.mouse.y = view.getInt32(5, true) - client.scrambleY; - } - - client.movePacketTriggered = true; - break; - case 17: - // Space Press - Split cell - this.pressSpace = true; - break; - case 18: - // Q Key Pressed - this.pressQ = true; - break; - case 19: - // Q Key Released - break; - case 21: - // W Press - Eject mass - this.pressW = true; - break; - case 254: - // Connection Start - if (view.byteLength == 5) { - this.protocolVersion = view.getUint32(1, true); - // Send on connection packets - this.socket.sendPacket(new Packet.ClearNodes(this.protocolVersion)); - var c = this.gameServer.config; - this.socket.sendPacket(new Packet.SetBorder( - c.borderLeft + this.socket.playerTracker.scrambleX, - c.borderRight + this.socket.playerTracker.scrambleX, - c.borderTop + this.socket.playerTracker.scrambleY, - c.borderBottom + this.socket.playerTracker.scrambleY - )); - } - break; - default: - break; - } -}; - -PacketHandler.prototype.setNickname = function(newNick) { - var client = this.socket.playerTracker; - if (client.cells.length < 1) { - // Set name first - client.setName(newNick); - - // Clear client's nodes - this.socket.sendPacket(new Packet.ClearNodes()); - - // If client has no cells... then spawn a player - this.gameServer.gameMode.onPlayerSpawn(this.gameServer, client); - - // Turn off spectate mode - client.spectate = false; - } -}; +var pjson = require('../package.json'); +var Packet = require('./packet'); +var BinaryReader = require('./packet/BinaryReader'); + +function PacketHandler(gameServer, socket) { + this.gameServer = gameServer; + this.socket = socket; + this.protocol = 0; + this.handshakeProtocol = null; + this.handshakeKey = null; + this.lastJoinTick = 0; + this.lastChatTick = 0; + this.lastStatTick = 0; + this.lastWTick = 0; + this.lastQTick = 0; + this.lastSpaceTick = 0; + + this.pressQ = false; + this.pressW = false; + this.pressSpace = false; + this.mouseData = null; + + this.handler = { + 254: this.handshake_onProtocol.bind(this), + }; +} + +module.exports = PacketHandler; + +PacketHandler.prototype.handleMessage = function (message) { + if (!this.handler.hasOwnProperty(message[0])) { + return; + } + this.handler[message[0]](message); + this.socket.lastAliveTime = this.gameServer.stepDateTime; +}; + +PacketHandler.prototype.handshake_onProtocol = function (message) { + if (message.length !== 5) return; + this.handshakeProtocol = message[1] | (message[2] << 8) | (message[3] << 16) | (message[4] << 24); + if (this.handshakeProtocol < 1 || this.handshakeProtocol > 9) { + this.socket.close(1002, "Not supported protocol"); + return; + } + this.handler = { + 255: this.handshake_onKey.bind(this), + }; +}; + +PacketHandler.prototype.handshake_onKey = function (message) { + if (message.length !== 5) return; + this.handshakeKey = message[1] | (message[2] << 8) | (message[3] << 16) | (message[4] << 24); + if (this.handshakeProtocol > 6 && this.handshakeKey !== 0) { + this.socket.close(1002, "Not supported protocol"); + return; + } + this.handshake_onCompleted(this.handshakeProtocol, this.handshakeKey); +}; + +PacketHandler.prototype.handshake_onCompleted = function (protocol, key) { + this.handler = { + 0: this.message_onJoin.bind(this), + 1: this.message_onSpectate.bind(this), + 16: this.message_onMouse.bind(this), + 17: this.message_onKeySpace.bind(this), + 18: this.message_onKeyQ.bind(this), + //19: AFK + 21: this.message_onKeyW.bind(this), + 99: this.message_onChat.bind(this), + 254: this.message_onStat.bind(this), + }; + this.protocol = protocol; + // Send handshake response + this.socket.sendPacket(new Packet.ClearAll()); + this.socket.sendPacket(new Packet.SetBorder(this.socket.playerTracker, this.gameServer.border, this.gameServer.config.serverGamemode, "MultiOgar " + pjson.version)); + // Send welcome message + this.gameServer.sendChatMessage(null, this.socket.playerTracker, "MultiOgar " + pjson.version); + if (this.gameServer.config.serverWelcome1) + this.gameServer.sendChatMessage(null, this.socket.playerTracker, this.gameServer.config.serverWelcome1); + if (this.gameServer.config.serverWelcome2) + this.gameServer.sendChatMessage(null, this.socket.playerTracker, this.gameServer.config.serverWelcome2); + if (this.gameServer.config.serverChat == 0) + this.gameServer.sendChatMessage(null, this.socket.playerTracker, "This server's chat is disabled."); + if (this.protocol < 4) + this.gameServer.sendChatMessage(null, this.socket.playerTracker, "WARNING: Protocol " + this.protocol + " assumed as 4!"); +}; + + +PacketHandler.prototype.message_onJoin = function (message) { + var tick = this.gameServer.getTick(); + var dt = tick - this.lastJoinTick; + this.lastJoinTick = tick; + if (dt < 25 || this.socket.playerTracker.cells.length !== 0) { + return; + } + var reader = new BinaryReader(message); + reader.skipBytes(1); + var text = null; + if (this.protocol < 6) + text = reader.readStringZeroUnicode(); + else + text = reader.readStringZeroUtf8(); + this.setNickname(text); +}; + +PacketHandler.prototype.message_onSpectate = function (message) { + if (message.length !== 1 || this.socket.playerTracker.cells.length !== 0) { + return; + } + this.socket.playerTracker.spectate = true; +}; + +PacketHandler.prototype.message_onMouse = function (message) { + if (message.length !== 13 && message.length !== 9 && message.length !== 21) { + return; + } + this.mouseData = Buffer.concat([message]); +}; + +PacketHandler.prototype.message_onKeySpace = function (message) { + if (message.length !== 1) return; + var tick = this.gameServer.getTick(); + var dt = tick - this.lastSpaceTick; + if (dt < this.gameServer.config.ejectCooldown) { + return; + } + this.lastSpaceTick = tick; + this.pressSpace = true; +}; + +PacketHandler.prototype.message_onKeyQ = function (message) { + if (message.length !== 1) return; + var tick = this.gameServer.getTick(); + var dt = tick - this.lastQTick; + if (dt < this.gameServer.config.ejectCooldown) { + return; + } + this.lastQTick = tick; + this.pressQ = true; +}; + +PacketHandler.prototype.message_onKeyW = function (message) { + if (message.length !== 1) return; + var tick = this.gameServer.getTick(); + var dt = tick - this.lastWTick; + if (dt < this.gameServer.config.ejectCooldown) { + return; + } + this.lastWTick = tick; + this.pressW = true; +}; + +PacketHandler.prototype.message_onChat = function (message) { + if (message.length < 3) return; + var tick = this.gameServer.getTick(); + var dt = tick - this.lastChatTick; + this.lastChatTick = tick; + if (dt < 25 * 2) { + return; + } + + var flags = message[1]; // flags + var rvLength = (flags & 2 ? 4:0) + (flags & 4 ? 8:0) + (flags & 8 ? 16:0); + if (message.length < 3 + rvLength) // second validation + return; + + var reader = new BinaryReader(message); + reader.skipBytes(2 + rvLength); // reserved + var text = null; + if (this.protocol < 6) + text = reader.readStringZeroUnicode(); + else + text = reader.readStringZeroUtf8(); + this.gameServer.onChatMessage(this.socket.playerTracker, null, text); +}; + +PacketHandler.prototype.message_onStat = function (message) { + if (message.length !== 1) return; + var tick = this.gameServer.getTick(); + var dt = tick - this.lastStatTick; + this.lastStatTick = tick; + if (dt < 25) { + return; + } + this.socket.sendPacket(new Packet.ServerStat(this.socket.playerTracker)); +}; + +PacketHandler.prototype.processMouse = function () { + if (this.mouseData == null) return; + var client = this.socket.playerTracker; + var reader = new BinaryReader(this.mouseData); + reader.skipBytes(1); + if (this.mouseData.length === 13) { + // protocol late 5, 6, 7 + client.mouse.x = reader.readInt32() - client.scrambleX; + client.mouse.y = reader.readInt32() - client.scrambleY; + } else if (this.mouseData.length === 9) { + // early protocol 5 + client.mouse.x = reader.readInt16() - client.scrambleX; + client.mouse.y = reader.readInt16() - client.scrambleY; + } else if (this.mouseData.length === 21) { + // protocol 4 + var x = reader.readDouble() - client.scrambleX; + var y = reader.readDouble() - client.scrambleY; + if (!isNaN(x) && !isNaN(y)) { + client.mouse.x = x; + client.mouse.y = y; + } + } + this.mouseData = null; +}; + +PacketHandler.prototype.process = function () { + if (this.pressSpace) { // Split cell + this.socket.playerTracker.pressSpace(); + this.pressSpace = false; + } + if (this.pressW) { // Eject mass + this.socket.playerTracker.pressW(); + this.pressW = false; + } + if (this.pressQ) { // Q Press + this.socket.playerTracker.pressQ(); + this.pressQ = false; + } + this.processMouse(); +}; + +PacketHandler.prototype.setNickname = function (text) { + var name = ""; + var skin = null; + if (text != null && text.length > 0) { + var skinName = null; + var userName = text; + var n = -1; + if (text[0] == '<' && (n = text.indexOf('>', 1)) >= 1) { + if (n > 1) + skinName = "%" + text.slice(1, n); + else + skinName = ""; + userName = text.slice(n + 1); + } + //else if (text[0] == "|" && (n = text.indexOf('|', 1)) >= 0) { + // skinName = ":http://i.imgur.com/" + text.slice(1, n) + ".png"; + // userName = text.slice(n + 1); + //} + if (skinName && !this.gameServer.checkSkinName(skinName)) { + skinName = null; + userName = text; + } + skin = skinName; + name = userName; + } + if (name.length > this.gameServer.config.playerMaxNickLength) { + name = name.substring(0, this.gameServer.config.playerMaxNickLength); + } + if (this.gameServer.checkBadWord(name)) { + skin = null; + name = "Hi there!"; + } + this.socket.playerTracker.joinGame(name, skin); +}; diff --git a/src/PlayerTracker.js b/src/PlayerTracker.js index 3972b6844..bcd294261 100644 --- a/src/PlayerTracker.js +++ b/src/PlayerTracker.js @@ -1,503 +1,582 @@ -var Packet = require('./packet'); -var GameServer = require('./GameServer'); - -function PlayerTracker(gameServer, socket) { - this.pID = -1; - this.disconnect = -1; // Disconnection - this.name = ""; - this.gameServer = gameServer; - this.socket = socket; - this.nodeAdditionQueue = []; - this.nodeDestroyQueue = []; - this.visibleNodes = []; - this.collidingNodes = []; // Perfomance save; all nodes colliding with player's cells - this.cells = []; - this.mergeOverride = false; // Triggered by console command - this.score = 0; // Needed for leaderboard - - this.mouse = { - x: 0, - y: 0 - }; - this.shouldMoveCells = true; // False if the mouse packet wasn't triggered - this.notMoved = false; // If one of cells have been moved after splitting this is triggered - this.movePacketTriggered = false; - this.ignoreNextMoveTick = false; // Screen mouse matches old screen mouse - this.mouseCells = []; // For individual cell movement - this.tickLeaderboard = 0; - this.tickViewBox = 0; - this.ticksLeft = 0; // Individual updates - this.cellTicksLeft = 0; // Individual updates for cells - - this.team = 0; - this.spectate = false; - this.freeRoam = false; // Free-roam mode enables player to move in spectate mode - - // Anti-teaming - this.massDecayMult = 1; // Anti-teaming multiplier - this.Wmult = 0; // W press multiplier, which will also account on duration of effect - this.checkForWMult = false; // Prevent oveload with W multiplier - this.virusMult = 0; // Virus explosion multiplier - this.splittingMult = 0; // Splitting multiplier - - // Viewing box - this.sightRangeX = 0; - this.sightRangeY = 0; - this.centerPos = { // Center of map - x: 3000, - y: 3000 - }; - this.viewBox = { - topY: 0, - bottomY: 0, - leftX: 0, - rightX: 0, - width: 0, // Half-width - height: 0 // Half-height - }; - - // Scramble the coordinate system for anti-raga - this.scrambleX = 0; - this.scrambleY = 0; - - // Gamemode function - if (gameServer) { - // Find center - this.centerPos.x = (gameServer.config.borderLeft - gameServer.config.borderRight) / 2; - this.centerPos.y = (gameServer.config.borderTop - gameServer.config.borderBottom) / 2; - // Player id - this.pID = gameServer.getNewPlayerID(); - // Gamemode function - gameServer.gameMode.onPlayerInit(this); - // Only scramble if enabled in config - if (gameServer.config.serverScrambleCoords == 1) { - this.scrambleX = Math.floor((1 << 15) * Math.random()); - this.scrambleY = Math.floor((1 << 15) * Math.random()); - } - } -} - -module.exports = PlayerTracker; - -// Setters/Getters - -PlayerTracker.prototype.setName = function(name) { - this.name = name; -}; - -PlayerTracker.prototype.getName = function() { - return this.name; -}; - -PlayerTracker.prototype.getScore = function(reCalcScore) { - if (reCalcScore) { - var s = 0; - for (var i = 0; i < this.cells.length; i++) { - if (!this.cells[i]) return; // Error - s += this.cells[i].mass; - this.score = s; - } - } - return this.score >> 0; -}; - -PlayerTracker.prototype.setColor = function(color) { - this.color.r = color.r; - this.color.g = color.g; - this.color.b = color.b; -}; - -PlayerTracker.prototype.getTeam = function() { - return this.team; -}; - -// Functions - -PlayerTracker.prototype.update = function() { - // Async update, perfomance reasons - setTimeout(function() { - // Don't send any messages if client didn't respond with protocol version - if (this.socket.packetHandler.protocolVersion == 0) return; - - // First reset colliding nodes - this.collidingNodes = []; - - // Move packet update - if (this.movePacketTriggered) { - this.movePacketTriggered = false; - this.shouldMoveCells = true; - } else { - this.shouldMoveCells = false; - } - // Actions buffer (So that people cant spam packets) - if (this.socket.packetHandler.pressSpace) { // Split cell - if (!this.mergeOverride) this.gameServer.gameMode.pressSpace(this.gameServer, this); - this.socket.packetHandler.pressSpace = false; - } - - if (this.socket.packetHandler.pressW) { // Eject mass - this.gameServer.gameMode.pressW(this.gameServer, this); - this.socket.packetHandler.pressW = false; - this.checkForWMult = true; - } - - if (this.socket.packetHandler.pressQ) { // Q Press - this.gameServer.gameMode.pressQ(this.gameServer, this); - this.socket.packetHandler.pressQ = false; - } - - var updateNodes = []; // Nodes that need to be updated via packet - - // Remove nodes from visible nodes if possible - var d = 0; - while (d < this.nodeDestroyQueue.length) { - var index = this.visibleNodes.indexOf(this.nodeDestroyQueue[d]); - if (index > -1) { - this.visibleNodes.splice(index, 1); - d++; // Increment - } else { - // Node was never visible anyways - this.nodeDestroyQueue.splice(d, 1); - } - } - - // Get visible nodes every 400 ms - var nonVisibleNodes = []; // Nodes that are not visible - if (this.tickViewBox <= 0) { - var newVisible = this.calcViewBox(); - try { // Add a try block in any case - - // Compare and destroy nodes that are not seen - for (var i = 0; i < this.visibleNodes.length; i++) { - var index = newVisible.indexOf(this.visibleNodes[i]); - if (index == -1) { - // Not seen by the client anymore - nonVisibleNodes.push(this.visibleNodes[i]); - } - } - - // Add nodes to client's screen if client has not seen it already - for (var i = 0; i < newVisible.length; i++) { - var index = this.visibleNodes.indexOf(newVisible[i]); - if (index == -1) { - updateNodes.push(newVisible[i]); - } - } - } catch(err) { - console.error(err); - } - - this.visibleNodes = newVisible; - // Reset Ticks - this.tickViewBox = 0; - } else { - this.tickViewBox--; - // Add nodes to screen - for (var i = 0; i < this.nodeAdditionQueue.length; i++) { - var node = this.nodeAdditionQueue[i]; - this.visibleNodes.push(node); - updateNodes.push(node); - } - } - - // Update moving nodes - for (var i = 0; i < this.visibleNodes.length; i++) { - var node = this.visibleNodes[i]; - if (node.sendUpdate()) { - // Sends an update if cell is moving - updateNodes.push(node); - } - } - - // Send packet - this.socket.sendPacket(new Packet.UpdateNodes( - this.nodeDestroyQueue, - updateNodes, - nonVisibleNodes, - this.scrambleX, - this.scrambleY - )); - - this.nodeDestroyQueue = []; // Reset destroy queue - this.nodeAdditionQueue = []; // Reset addition queue - - // Update leaderboard - if (this.tickLeaderboard <= 0) { - this.socket.sendPacket(new Packet.UpdateLeaderboard( - this.gameServer.leaderboard, - this.gameServer.gameMode.packetLB, - this.protocolVersion, - this.pID - )); - this.tickLeaderboard = 10; // 20 ticks = 1 second - } else { - this.tickLeaderboard--; - } - - // Map obfuscation - var width = this.viewBox.width; - var height = this.viewBox.height; - - if (this.cells.length == 0 && this.gameServer.config.serverScrambleMinimaps >= 1) { - // Update map, it may have changed - this.socket.sendPacket(new Packet.SetBorder( - -this.gameServer.config.borderLeft + this.scrambleX, - this.gameServer.config.borderRight + this.scrambleX, - -this.gameServer.config.borderTop + this.scrambleY, - this.gameServer.config.borderBottom + this.scrambleY - )); - } else { - // Send a border packet to fake the map size - this.socket.sendPacket(new Packet.SetBorder( - Math.max(this.centerPos.x + this.scrambleX - width, -this.gameServer.config.borderLeft + this.scrambleX), - Math.min(this.centerPos.x + this.scrambleX + width, this.gameServer.config.borderRight + this.scrambleX), - Math.max(this.centerPos.y + this.scrambleY - height, -this.gameServer.config.borderTop + this.scrambleY), - Math.min(this.centerPos.y + this.scrambleY + height, this.gameServer.config.borderBottom + this.scrambleY) - )); - } - - // Handles disconnections - if (this.disconnect > -1) { - // Player has disconnected... remove it when the timer hits -1 - this.disconnect--; - // Also remove it when its cells are completely eaten not to back up dead clients - if (this.disconnect == -1 || this.cells.length == 0) { - // Remove all client cells - var len = this.cells.length; - for (var i = 0; i < len; i++) { - var cell = this.socket.playerTracker.cells[0]; - - if (!cell) { - continue; - } - - this.gameServer.removeNode(cell); - } - - // Remove from client list - var index = this.gameServer.clients.indexOf(this.socket); - if (index != -1) { - this.gameServer.clients.splice(index, 1); - } - } - } - }.bind(this), 0); -}; - -PlayerTracker.prototype.antiTeamTick = function() { - // ANTI-TEAMING DECAY - // Calculated even if anti-teaming is disabled. - var effectSum = this.Wmult + this.virusMult + this.splittingMult; - if (this.Wmult - 0.00028 > 0) this.Wmult -= 0.00028; - this.virusMult *= 0.999; - this.splittingMult *= 0.9982; - // Apply anti-teaming if required - if (effectSum > 2) this.massDecayMult = Math.min(effectSum / 2, 3.14); - else this.massDecayMult = 1; -}; - -PlayerTracker.prototype.applyTeaming = function(x, type) { - // Called when player does an action which increases anti-teaming - var effectSum = this.Wmult + this.virusMult + this.splittingMult; - - // Applied anti-teaming is 1.5x smaller if over the threshold - var n = effectSum > 1.5 ? x : x / 1.5; - - switch (type) { - case 0: // Ejected cell - this.Wmult += n; - break; - case 1: // Virus explosion - this.virusMult += n; - break; - case 2: // Splitting - this.splittingMult += n; - break; - } -}; - -// Viewing box - -PlayerTracker.prototype.updateSightRange = function() { // For view distance - var totalSize = 1.0; - var len = this.cells.length; - - for (var i = 0; i < len; i++) { - if (!this.cells[i]) { - continue; - } - - totalSize += this.cells[i].getSize(); - } - - var factor = Math.pow(Math.min(64.0 / totalSize, 1), 0.4); - this.sightRangeX = this.gameServer.config.serverViewBaseX / factor; - this.sightRangeY = this.gameServer.config.serverViewBaseY / factor; -}; - -PlayerTracker.prototype.updateCenter = function() { // Get center of cells - var len = this.cells.length; - - if (len <= 0) return; - - var X = 0; - var Y = 0; - for (var i = 0; i < len; i++) { - // Error check - if (!this.cells[i]) { - len--; - continue; - } - var cell = this.cells[i]; - - X += cell.position.x; - Y += cell.position.y; - } - - this.centerPos.x = X / len; - this.centerPos.y = Y / len; -}; - -PlayerTracker.prototype.calcViewBox = function() { - if (this.spectate) { - // Spectate mode - return this.getSpectateNodes(); - } - - // Main function - this.updateSightRange(); - this.updateCenter(); - - // Box - this.viewBox.topY = this.centerPos.y - this.sightRangeY; - this.viewBox.bottomY = this.centerPos.y + this.sightRangeY; - this.viewBox.leftX = this.centerPos.x - this.sightRangeX; - this.viewBox.rightX = this.centerPos.x + this.sightRangeX; - this.viewBox.width = this.sightRangeX; - this.viewBox.height = this.sightRangeY; - - var newVisible = this.calcVisibleNodes(); - - return newVisible; -}; - -PlayerTracker.prototype.getSpectateNodes = function() { - var specPlayer = this.gameServer.largestClient; - - if (!this.freeRoam) { - - if (!specPlayer) return this.moveInFreeRoam(); // There are probably no players - - // Get spectate player's location and calculate zoom amount - var specZoom = Math.min(Math.sqrt(100 * specPlayer.getScore(false)), 555); - specZoom = Math.pow(Math.min(40.5 / specZoom, 1.0), 0.4); - - this.setCenterPos(specPlayer.centerPos.x, specPlayer.centerPos.y); - this.sendPosPacket(specZoom); - - return specPlayer.visibleNodes.slice(0); - } - // Behave like client is in free-roam as function didn't return nodes - return this.moveInFreeRoam(); -}; - -PlayerTracker.prototype.moveInFreeRoam = function() { - // User is in free roam - // To mimic agar.io, get distance from center to mouse and apply a part of the distance - - var dist = this.gameServer.getDist(this.mouse.x, this.mouse.y, this.centerPos.x, this.centerPos.y); - var angle = this.getAngle(this.mouse.x, this.mouse.y, this.centerPos.x, this.centerPos.y); - var speed = Math.min(dist / 10, 70); // Not to break laws of universe by going faster than light speed - - this.centerPos.x += speed * Math.sin(angle); - this.centerPos.y += speed * Math.cos(angle); - - // Check if went away from borders - this.checkBorderPass(); - - // Now that we've updated center pos, get nearby cells - // We're going to use config's view base times 2.5 - - var mult = 3.5; // To simplify multiplier, in case this needs editing later on - var baseX = this.gameServer.config.serverViewBaseX; - var baseY = this.gameServer.config.serverViewBaseY; - - this.viewBox.topY = this.centerPos.y - baseY * mult; - this.viewBox.bottomY = this.centerPos.y + baseY * mult; - this.viewBox.leftX = this.centerPos.x - baseX * mult; - this.viewBox.rightX = this.centerPos.x + baseX * mult; - this.viewBox.width = baseX * mult; - this.viewBox.height = baseY * mult; - - // Use calcViewBox's way of looking for nodes - var newVisible = this.calcVisibleNodes(); - var specZoom = 222; - specZoom = Math.pow(Math.min(40.5 / specZoom, 1.0), 0.4) * 0.6; // Constant zoom - this.sendPosPacket(specZoom); - return newVisible; -}; - -PlayerTracker.prototype.calcVisibleNodes = function() { - var newVisible = []; - for (var i = 0; i < this.gameServer.nodes.length; i++) { - var node = this.gameServer.nodes[i]; - if (!node) { - continue; - } - - var check = node.visibleCheck(this.viewBox, this.centerPos, this.cells); - if (check > 0 || node.owner == this) { - // Cell is in range of viewBox - newVisible.push(node); - // Check if it's colliding with one of player's cells - if (check == 2) this.collidingNodes.push(node); - } - } - return newVisible; -}; - -PlayerTracker.prototype.setCenterPos = function(x, y) { - this.centerPos.x = x; - this.centerPos.y = y; - if (this.freeRoam) this.checkBorderPass(); -}; - -PlayerTracker.prototype.checkBorderPass = function() { - // A check while in free-roam mode to avoid player going into nothingness - if (this.centerPos.x < -this.gameServer.config.borderLeft) { - this.centerPos.x = this.gameServer.config.borderLeft; - } - if (this.centerPos.x > this.gameServer.config.borderRight) { - this.centerPos.x = this.gameServer.config.borderRight; - } - if (this.centerPos.y < -this.gameServer.config.borderTop) { - this.centerPos.y = this.gameServer.config.borderTop; - } - if (this.centerPos.y > this.gameServer.config.borderBottom) { - this.centerPos.y = this.gameServer.config.borderBottom; - } -}; - -PlayerTracker.prototype.sendPosPacket = function(specZoom) { - // TODO: Send packet elsewhere so it is sent more often - this.socket.sendPacket(new Packet.UpdatePosition( - this.centerPos.x + this.scrambleX, - this.centerPos.y + this.scrambleY, - specZoom - )); -}; - -PlayerTracker.prototype.sendCustomPosPacket = function(x, y, specZoom) { - // TODO: Send packet elsewhere so it is sent more often - this.socket.sendPacket(new Packet.UpdatePosition( - x + this.scrambleX, - y + this.scrambleY, - specZoom - )); -}; - -PlayerTracker.prototype.getAngle = function(x1, y1, x2, y2) { - var deltaY = y1 - y2; - var deltaX = x1 - x2; - return Math.atan2(deltaX, deltaY); -}; +var Packet = require('./packet'); +var GameServer = require('./GameServer'); +var BinaryWriter = require("./packet/BinaryWriter"); +var UserRoleEnum = require("./enum/UserRoleEnum"); + +function PlayerTracker(gameServer, socket) { + this.gameServer = gameServer; + this.socket = socket; + this.pID = -1; + this.userRole = UserRoleEnum.GUEST; + this.userAuth = null; + this.isRemoved = false; + this.isCloseRequested = false; + this._name = ""; + this._skin = ""; + this._nameUtf8 = null; + this._nameUnicode = null; + this._skinUtf8 = null; + this.color = { r: 0, g: 0, b: 0 }; + this.viewNodes = []; + this.clientNodes = []; + this.cells = []; + this.mergeOverride = false; // Triggered by console command + this._score = 0; // Needed for leaderboard + this._scale = 1; + this._scaleF = 1; + this.isMassChanged = true; + this.borderCounter = 0; + + this.mouse = { + x: 0, + y: 0 + }; + this.tickLeaderboard = 0; + + this.team = 0; + this.spectate = false; + this.freeRoam = false; // Free-roam mode enables player to move in spectate mode + this.spectateTarget = null; // Spectate target, null for largest player + this.lastSpectateSwitchTick = 0; + + this.centerPos = { + x: 0, + y: 0 + }; + this.viewBox = { + minx: 0, + miny: 0, + maxx: 0, + maxy: 0, + width: 0, + height: 0, + halfWidth: 0, + halfHeight: 0 + }; + + // Scramble the coordinate system for anti-raga + this.scrambleX = 0; + this.scrambleY = 0; + this.scrambleId = 0; + + this.connectedTime = 0; + this.isMinion = false; + this.spawnCounter = 0; + this.isMuted = false; + + // Gamemode function + if (gameServer) { + this.connectedTime = gameServer.stepDateTime; + this.centerPos.x = gameServer.border.centerx; + this.centerPos.y = gameServer.border.centery; + // Player id + this.pID = gameServer.getNewPlayerID(); + // Gamemode function + gameServer.gameMode.onPlayerInit(this); + // Only scramble if enabled in config + this.scramble(); + } +} + +module.exports = PlayerTracker; + +// Setters/Getters + +PlayerTracker.prototype.scramble = function () { + if (!this.gameServer.config.serverScrambleLevel) { + this.scrambleId = 0; + this.scrambleX = 0; + this.scrambleY = 0; + } else { + this.scrambleId = (Math.random() * 0xFFFFFFFF) >>> 0; + // avoid mouse packet limitations + var maxx = Math.max(0, 32767 - 1000 - this.gameServer.border.width); + var maxy = Math.max(0, 32767 - 1000 - this.gameServer.border.height); + var x = maxx * Math.random(); + var y = maxy * Math.random(); + if (Math.random() >= 0.5) x = -x; + if (Math.random() >= 0.5) y = -y; + this.scrambleX = x; + this.scrambleY = y; + } + this.borderCounter = 0; +}; + +PlayerTracker.prototype.getFriendlyName = function () { + var name = this.getName(); + if (!name) name = ""; + name = name.trim(); + if (name.length == 0) + name = "An unnamed cell"; + return name; +}; + +PlayerTracker.prototype.setName = function (name) { + this._name = name; + if (!name || name.length < 1) { + this._nameUnicode = null; + this._nameUtf8 = null; + return; + } + var writer = new BinaryWriter() + writer.writeStringZeroUnicode(name); + this._nameUnicode = writer.toBuffer(); + writer = new BinaryWriter() + writer.writeStringZeroUtf8(name); + this._nameUtf8 = writer.toBuffer(); +}; + +PlayerTracker.prototype.getName = function () { + return this._name; +}; + +PlayerTracker.prototype.setSkin = function (skin) { + this._skin = skin; + if (!skin || skin.length < 1) { + this._skinUtf8 = null; + return; + } + var writer = new BinaryWriter() + writer.writeStringZeroUtf8(skin); + this._skinUtf8 = writer.toBuffer(); +}; + +PlayerTracker.prototype.getSkin = function () { + if (this.gameServer.gameMode.haveTeams) { + return ""; + } + return this._skin; +}; + +PlayerTracker.prototype.getNameUtf8 = function () { + return this._nameUtf8; +} + +PlayerTracker.prototype.getNameUnicode = function () { + return this._nameUnicode; +} + +PlayerTracker.prototype.getSkinUtf8 = function () { + return this._skinUtf8; +} + +PlayerTracker.prototype.getColor = function (color) { + return this.color; +}; + +PlayerTracker.prototype.setColor = function (color) { + this.color.r = color.r; + this.color.g = color.g; + this.color.b = color.b; +}; + +PlayerTracker.prototype.getTeam = function () { + return this.team; +}; + +PlayerTracker.prototype.getScore = function () { + if (this.isMassChanged) + this.updateMass(); + return this._score; +}; + +PlayerTracker.prototype.getScale = function () { + if (this.isMassChanged) + this.updateMass(); + return this._scale; +}; + +PlayerTracker.prototype.updateMass = function () { + var totalSize = 0; + var totalScore = 0; + for (var i = 0; i < this.cells.length; i++) { + var node = this.cells[i]; + totalSize += node.getSize(); + totalScore += node.getSizeSquared(); + } + if (totalSize == 0) { + //do not change scale for spectators or not in game players + this._score = 0; + } else { + this._score = totalScore; + this._scale = Math.pow(Math.min(64 / totalSize, 1), 0.4); + } + this.isMassChanged = false; +}; + +PlayerTracker.prototype.massChanged = function () { + this.isMassChanged = true; +}; + +// Functions + +PlayerTracker.prototype.joinGame = function (name, skin) { + if (this.cells.length > 0) return; + if (name == null) name = ""; + this.setName(name); + if (skin != null) + this.setSkin(skin); + this.spectate = false; + this.freeRoam = false; + this.spectateTarget = null; + + // some old clients don't understand ClearAll message + // so we will send update for them + if (this.socket.packetHandler.protocol < 6) { + this.socket.sendPacket(new Packet.UpdateNodes(this, [], [], [], this.clientNodes)); + } + this.socket.sendPacket(new Packet.ClearAll()); + this.clientNodes = []; + this.scramble(); + if (this.gameServer.config.serverScrambleLevel < 2) { + // no scramble / lightweight scramble + this.socket.sendPacket(new Packet.SetBorder(this, this.gameServer.border)); + } + else if (this.gameServer.config.serverScrambleLevel == 3) { + // Scramble level 3 (no border) + // Ruins most known minimaps + var border = { + minx: this.gameServer.border.minx - (0x10000 + 10000000 * Math.random()), + miny: this.gameServer.border.miny - (0x10000 + 10000000 * Math.random()), + maxx: this.gameServer.border.maxx + (0x10000 + 10000000 * Math.random()), + maxy: this.gameServer.border.maxy + (0x10000 + 10000000 * Math.random()) + }; + this.socket.sendPacket(new Packet.SetBorder(this, border)); + } + this.spawnCounter++; + this.gameServer.gameMode.onPlayerSpawn(this.gameServer, this); +}; + +PlayerTracker.prototype.checkConnection = function () { + // Handle disconnection + if (!this.socket.isConnected) { + // wait for playerDisconnectTime + var dt = (this.gameServer.stepDateTime - this.socket.closeTime) / 1000; + if (this.cells.length == 0 || dt >= this.gameServer.config.playerDisconnectTime) { + // Remove all client cells + var cells = this.cells; + this.cells = []; + for (var i = 0; i < cells.length; i++) { + this.gameServer.removeNode(cells[i]); + } + // Mark to remove + this.isRemoved = true; + return; + } + this.mouse.x = this.centerPos.x; + this.mouse.y = this.centerPos.y; + this.socket.packetHandler.pressSpace = false; + this.socket.packetHandler.pressW = false; + this.socket.packetHandler.pressQ = false; + return; + } + // Check timeout + if (!this.isCloseRequested && this.gameServer.config.serverTimeout) { + var dt = (this.gameServer.stepDateTime - this.socket.lastAliveTime) / 1000; + if (dt >= this.gameServer.config.serverTimeout) { + this.socket.close(1000, "Connection timeout"); + this.isCloseRequested = true; + } + } +}; + +PlayerTracker.prototype.updateTick = function () { + this.socket.packetHandler.process(); + if (this.spectate) { + if (this.freeRoam || this.getSpectateTarget() == null) { + // free roam + this.updateCenterFreeRoam(); + this._scale = this.gameServer.config.serverSpectatorScale;//0.25; + } else { + // spectate target + return; + } + } else { + // in game + this.updateCenterInGame(); + } + this.updateViewBox(); + this.updateVisibleNodes(); +}; + +PlayerTracker.prototype.sendUpdate = function () { + if (this.isRemoved|| + !this.socket.packetHandler.protocol || + !this.socket.isConnected || + (this.socket._socket.writable != null && !this.socket._socket.writable) || + this.socket.readyState != this.socket.OPEN) { + // do not send update for disconnected clients + // also do not send if initialization is not complete yet + return; + } + + if (this.spectate) { + if (!this.freeRoam) { + // spectate target + var player = this.getSpectateTarget(); + if (player != null) { + this.setCenterPos(player.centerPos.x, player.centerPos.y); + this._scale = player.getScale(); + this.viewBox = player.viewBox; + this.viewNodes = player.viewNodes; + } + } + this.sendCameraPacket(); + } + + if (this.gameServer.config.serverScrambleLevel == 2) { + // scramble (moving border) + if (this.borderCounter == 0) { + var bound = { + minx: Math.max(this.gameServer.border.minx, this.viewBox.minx - this.viewBox.halfWidth), + miny: Math.max(this.gameServer.border.miny, this.viewBox.miny - this.viewBox.halfHeight), + maxx: Math.min(this.gameServer.border.maxx, this.viewBox.maxx + this.viewBox.halfWidth), + maxy: Math.min(this.gameServer.border.maxy, this.viewBox.maxy + this.viewBox.halfHeight) + }; + this.socket.sendPacket(new Packet.SetBorder(this, bound)); + } + this.borderCounter++; + if (this.borderCounter >= 20) + this.borderCounter = 0; + } + + var delNodes = []; + var eatNodes = []; + var addNodes = []; + var updNodes = []; + var oldIndex = 0; + var newIndex = 0; + for (; newIndex < this.viewNodes.length && oldIndex < this.clientNodes.length;) { + if (this.viewNodes[newIndex].nodeId < this.clientNodes[oldIndex].nodeId) { + addNodes.push(this.viewNodes[newIndex]); + newIndex++; + continue; + } + if (this.viewNodes[newIndex].nodeId > this.clientNodes[oldIndex].nodeId) { + var node = this.clientNodes[oldIndex]; + if (node.isRemoved && node.getKiller() != null && node.owner != node.getKiller().owner) + eatNodes.push(node); + else + delNodes.push(node); + oldIndex++; + continue; + } + var node = this.viewNodes[newIndex]; + // skip food & eject if no moving + if (node.isMoving || (node.cellType != 1 && node.cellType != 3)) + updNodes.push(node); + newIndex++; + oldIndex++; + } + for (; newIndex < this.viewNodes.length; ) { + addNodes.push(this.viewNodes[newIndex]); + newIndex++; + } + for (; oldIndex < this.clientNodes.length; ) { + var node = this.clientNodes[oldIndex]; + if (node.isRemoved && node.getKiller() != null && node.owner != node.getKiller().owner) + eatNodes.push(node); + else + delNodes.push(node); + oldIndex++; + } + this.clientNodes = this.viewNodes; + + // Send packet + this.socket.sendPacket(new Packet.UpdateNodes( + this, + addNodes, + updNodes, + eatNodes, + delNodes)); + + // Update leaderboard + if (++this.tickLeaderboard > 25) { + // 1 / 0.040 = 25 (once per second) + this.tickLeaderboard = 0; + if (this.gameServer.leaderboardType >= 0) { + var packet = new Packet.UpdateLeaderboard(this, this.gameServer.leaderboard, this.gameServer.leaderboardType); + this.socket.sendPacket(packet); + } + } +}; + +// Viewing box + +PlayerTracker.prototype.updateCenterInGame = function () { // Get center of cells + var len = this.cells.length; + if (len <= 0) return; + var cx = 0; + var cy = 0; + var count = 0; + for (var i = 0; i < len; i++) { + var node = this.cells[i]; + cx += node.position.x; + cy += node.position.y; + count++; + } + if (count == 0) return; + cx /= count; + cy /= count; + cx = (this.centerPos.x + cx) / 2; + cy = (this.centerPos.y + cy) / 2; + this.setCenterPos(cx, cy); +}; + +PlayerTracker.prototype.updateCenterFreeRoam = function () { + var dx = this.mouse.x - this.centerPos.x; + var dy = this.mouse.y - this.centerPos.y; + var squared = dx * dx + dy * dy; + if (squared < 1) return; // stop threshold + + // distance + var d = Math.sqrt(squared); + + var invd = 1 / d; + var nx = dx * invd; + var ny = dy * invd; + + var speed = Math.min(d, 32); + if (speed <= 0) return; + + var x = this.centerPos.x + nx * speed; + var y = this.centerPos.y + ny * speed; + this.setCenterPos(x, y); +}; + +PlayerTracker.prototype.updateViewBox = function () { + var scale = this.getScale(); + scale = Math.max(scale, this.gameServer.config.serverMinScale); + this._scaleF += 0.1 * (scale - this._scaleF); + if (isNaN(this._scaleF)) + this._scaleF = 1; + var width = (this.gameServer.config.serverViewBaseX + 100) / this._scaleF; + var height = (this.gameServer.config.serverViewBaseY + 100) / this._scaleF; + var halfWidth = width / 2; + var halfHeight = height / 2; + this.viewBox = { + minx: this.centerPos.x - halfWidth, + miny: this.centerPos.y - halfHeight, + maxx: this.centerPos.x + halfWidth, + maxy: this.centerPos.y + halfHeight, + width: width, + height: height, + halfWidth: halfWidth, + halfHeight: halfHeight + }; +}; + +PlayerTracker.prototype.pressQ = function () { + if (this.spectate) { + // Check for spam first (to prevent too many add/del updates) + var tick = this.gameServer.getTick(); + if (tick - this.lastSpectateSwitchTick < 40) + return; + this.lastSpectateSwitchTick = tick; + + if (this.spectateTarget == null) { + this.freeRoam = !this.freeRoam; + } + this.spectateTarget = null; + } +}; + +PlayerTracker.prototype.pressW = function () { + if (this.spectate) { + return; + } + else if (this.gameServer.run) { + this.gameServer.ejectMass(this); + } +}; + +PlayerTracker.prototype.pressSpace = function () { + if (this.spectate) { + // Check for spam first (to prevent too many add/del updates) + var tick = this.gameServer.getTick(); + if (tick - this.lastSpectateSwitchTick < 40) + return; + this.lastSpectateSwitchTick = tick; + + // Space doesn't work for freeRoam mode + if (this.freeRoam || this.gameServer.largestClient == null) + return; + this.nextSpectateTarget(); + } else if (this.gameServer.run) { + if (this.mergeOverride) + return; + this.gameServer.splitCells(this); + } +}; + +PlayerTracker.prototype.nextSpectateTarget = function () { + if (this.spectateTarget == null) { + this.spectateTarget = this.gameServer.largestClient; + return; + } + // lookup for next spectate target + var index = this.gameServer.clients.indexOf(this.spectateTarget.socket); + if (index < 0) { + this.spectateTarget = this.gameServer.largestClient; + return; + } + // find next + for (var i = index + 1; i < this.gameServer.clients.length; i++) { + var player = this.gameServer.clients[i].playerTracker; + if (player.cells.length > 0) { + this.spectateTarget = player; + return; + } + } + for (var i = 0; i <= index; i++) { + var player = this.gameServer.clients[i].playerTracker; + if (player.cells.length > 0) { + this.spectateTarget = player; + return; + } + } + // no alive players + this.spectateTarget = null; +}; + +PlayerTracker.prototype.getSpectateTarget = function () { + if (this.spectateTarget == null || this.spectateTarget.isRemoved || this.spectateTarget.cells.length < 1) { + this.spectateTarget = null; + return this.gameServer.largestClient; + } + return this.spectateTarget; +}; + +PlayerTracker.prototype.updateVisibleNodes = function () { + this.viewNodes = []; + if (!this.isMinion) { + var self = this; + this.gameServer.quadTree.find(this.viewBox, function (quadItem) { + if (quadItem.cell.owner != self) + self.viewNodes.push(quadItem.cell); + }); + } + this.viewNodes = this.viewNodes.concat(this.cells); + this.viewNodes.sort(function (a, b) { return a.nodeId - b.nodeId; }); +}; + +PlayerTracker.prototype.setCenterPos = function (x, y) { + if (isNaN(x) || isNaN(y)) { + throw new TypeError("PlayerTracker.setCenterPos: NaN"); + } + x = Math.max(x, this.gameServer.border.minx); + y = Math.max(y, this.gameServer.border.miny); + x = Math.min(x, this.gameServer.border.maxx); + y = Math.min(y, this.gameServer.border.maxy); + this.centerPos.x = x; + this.centerPos.y = y; +}; + +PlayerTracker.prototype.sendCameraPacket = function () { + this.socket.sendPacket(new Packet.UpdatePosition( + this, + this.centerPos.x, + this.centerPos.y, + this.getScale() + )); +}; diff --git a/src/ai/BotLoader.js b/src/ai/BotLoader.js index e0b5349b0..7f9004954 100644 --- a/src/ai/BotLoader.js +++ b/src/ai/BotLoader.js @@ -1,56 +1,51 @@ -// Project imports -var BotPlayer = require('./BotPlayer'); -var FakeSocket = require('./FakeSocket'); -var PacketHandler = require('../PacketHandler'); - -function BotLoader(gameServer) { - this.gameServer = gameServer; - this.loadNames(); -} - -module.exports = BotLoader; - -BotLoader.prototype.getName = function() { - var name = ""; - - // Picks a random name for the bot - if (this.randomNames.length > 0) { - var index = Math.floor(Math.random() * this.randomNames.length); - name = this.randomNames[index]; - this.randomNames.splice(index, 1); - } else { - name = "bot" + ++this.nameIndex; - } - - return name; -}; - -BotLoader.prototype.loadNames = function() { - this.randomNames = []; - - // Load names - try { - var fs = require("fs"); // Import the util library - - // Read and parse the names - filter out whitespace-only names - this.randomNames = fs.readFileSync("./botnames.txt", "utf8").split(/[\r\n]+/).filter(function(x) { - return x != ''; // filter empty names - }); - } catch (e) { - // Nothing, use the default names - } - - this.nameIndex = 0; -}; - -BotLoader.prototype.addBot = function() { - var s = new FakeSocket(this.gameServer); - s.playerTracker = new BotPlayer(this.gameServer, s); - s.packetHandler = new PacketHandler(this.gameServer, s); - - // Add to client list - this.gameServer.clients.push(s); - - // Add to world - s.packetHandler.setNickname(this.getName()); -}; +// Project imports +var fs = require("fs"); +var Logger = require('../modules/Logger'); +var BotPlayer = require('./BotPlayer'); +var FakeSocket = require('./FakeSocket'); +var PacketHandler = require('../PacketHandler'); + +function BotLoader(gameServer) { + this.gameServer = gameServer; + this.loadNames(); +} + +module.exports = BotLoader; + +BotLoader.prototype.getName = function () { + var name = ""; + + // Picks a random name for the bot + if (this.randomNames.length > 0) { + var index = (this.randomNames.length * Math.random()) >>> 0; + name = this.randomNames[index]; + } else { + name = "bot" + ++this.nameIndex; + } + + return name; +}; + +BotLoader.prototype.loadNames = function () { + this.randomNames = []; + + if (fs.existsSync("./botnames.txt")) { + // Read and parse the names - filter out whitespace-only names + this.randomNames = fs.readFileSync("./botnames.txt", "utf8").split(/[\r\n]+/).filter(function (x) { + return x != ''; // filter empty names + }); + } + this.nameIndex = 0; +}; + +BotLoader.prototype.addBot = function () { + var s = new FakeSocket(this.gameServer); + s.playerTracker = new BotPlayer(this.gameServer, s); + s.packetHandler = new PacketHandler(this.gameServer, s); + + // Add to client list + this.gameServer.clients.push(s); + + // Add to world + s.packetHandler.setNickname(this.getName()); +}; diff --git a/src/ai/BotPlayer.js b/src/ai/BotPlayer.js index d229b7dfa..94c86b2cf 100644 --- a/src/ai/BotPlayer.js +++ b/src/ai/BotPlayer.js @@ -1,11 +1,11 @@ -var PlayerTracker = require('../PlayerTracker'); +var PlayerTracker = require('../PlayerTracker'); var gameServer = require('../GameServer'); var Vector = require('vector2-node'); function BotPlayer() { PlayerTracker.apply(this, Array.prototype.slice.call(arguments)); - //this.color = gameServer.getRandomColor(); - + //this.setColor(gameServer.getRandomColor()); + this.splitCooldown = 0; } @@ -14,60 +14,52 @@ BotPlayer.prototype = new PlayerTracker(); // Functions -BotPlayer.prototype.getLowestCell = function() { +BotPlayer.prototype.getLowestCell = function () { // Gets the cell with the lowest mass if (this.cells.length <= 0) { return null; // Error! } - + // Sort the cells by Array.sort() function to avoid errors var sorted = this.cells.valueOf(); - sorted.sort(function(a, b) { - return b.mass - a.mass; + sorted.sort(function (a, b) { + return b.getSize() - a.getSize(); }); - + return sorted[0]; }; -BotPlayer.prototype.update = function() { // Overrides the update function from player tracker - // Remove nodes from visible nodes if possible - for (var i = 0; i < this.nodeDestroyQueue.length; i++) { - var index = this.visibleNodes.indexOf(this.nodeDestroyQueue[i]); - if (index > -1) { - this.visibleNodes.splice(index, 1); +BotPlayer.prototype.checkConnection = function () { + if (this.socket.isCloseRequest) { + while (this.cells.length > 0) { + this.gameServer.removeNode(this.cells[0]); } + this.isRemoved = true; + return; } - + // Respawn if bot is dead if (this.cells.length <= 0) { this.gameServer.gameMode.onPlayerSpawn(this.gameServer, this); if (this.cells.length == 0) { // If the bot cannot spawn any cells, then disconnect it this.socket.close(); - return; } } - +} + +BotPlayer.prototype.sendUpdate = function () { // Overrides the update function from player tracker if (this.splitCooldown > 0) this.splitCooldown--; - setTimeout(function() { - // Calculate nodes - this.visibleNodes = this.calcViewBox(); - - // Calc predators/prey - var cell = this.getLowestCell(); - - // Action - this.decide(cell); - - // Reset queues - this.nodeDestroyQueue = []; - this.nodeAdditionQueue = []; - }.bind(this), 0); + // Calc predators/prey + var cell = this.getLowestCell(); + + // Action + this.decide(cell); }; // Custom -BotPlayer.prototype.decide = function(cell) { +BotPlayer.prototype.decide = function (cell) { if (!cell) return; // Cell was eaten, check in the next tick (I'm too lazy) var cellPos = cell.position; @@ -76,41 +68,65 @@ BotPlayer.prototype.decide = function(cell) { var split = false, splitTarget = null, threats = []; - - for (var i = 0; i < this.visibleNodes.length; i++) { - var check = this.visibleNodes[i]; + + for (var i = 0; i < this.viewNodes.length; i++) { + var check = this.viewNodes[i]; + if (check.owner == this) continue; // Get attraction of the cells - avoid larger cells, viruses and same team cells var influence = 0; if (check.cellType == 0) { // Player cell - if (this.gameServer.gameMode.haveTeams && (cell.owner.team == check.owner.team)) influence = 0; // Same team cell - else if (cell.mass / 1.3 > check.mass) influence = check.getSize() * 2.5; // Can eat it - else if (check.mass / 1.3 > cell.mass) influence = -check.getSize(); // Can eat me + if (this.gameServer.gameMode.haveTeams && (cell.owner.team == check.owner.team)) { + // Same team cell + influence = 0; + } + else if (cell.getSize() > (check.getSize() + 4) * 1.15) { + // Can eat it + influence = check.getSize() * 2.5; + } + else if (check.getSize() + 4 > cell.getSize() * 1.15) { + // Can eat me + influence = -check.getSize(); + } else { + influence = -(check.getSize() / cell.getSize()) / 3; + } } else if (check.cellType == 1) { // Food influence = 1; } else if (check.cellType == 2) { // Virus - if (cell.mass / 1.3 > check.mass) { + if (cell.getSize() > check.getSize() * 1.15) { // Can eat it - if (this.cells.length == this.gameServer.config.playerMaxCells) influence = check.getSize() * 2.5; // Won't explode - else influence = -1; // Can explode + if (this.cells.length == this.gameServer.config.playerMaxCells) { + // Won't explode + influence = check.getSize() * 2.5; + } + else { + // Can explode + influence = -1; + } + } else if (check.isMotherCell && check.getSize() > cell.getSize() * 1.15) { + // can eat me + influence = -1; } } else if (check.cellType == 3) { // Ejected mass - if (cell.mass / 1.3 > check.mass) influence = check.getSize(); + if (cell.getSize() > check.getSize() * 1.15) + // can eat + influence = check.getSize(); } else { influence = check.getSize(); // Might be TeamZ } - + // Apply influence if it isn't 0 or my cell - if (influence == 0 || cell.owner == check.owner) continue; + if (influence == 0 || cell.owner == check.owner) + continue; // Calculate separation between cell and check var checkPos = check.position; var displacement = new Vector(checkPos.x - cellPos.x, checkPos.y - cellPos.y); - + // Figure out distance between cells var distance = displacement.length(); if (influence < 0) { @@ -118,21 +134,25 @@ BotPlayer.prototype.decide = function(cell) { distance -= cell.getSize() + check.getSize(); if (check.cellType == 0) threats.push(check); } - + // The farther they are the smaller influnce it is if (distance < 1) distance = 1; // Avoid NaN and positive influence with negative distance & attraction influence /= distance; - + // Produce force vector exerted by this entity on the cell var force = displacement.normalize().scale(influence); - + // Splitting conditions - if (check.cellType == 0 && cell.mass / 2.6 > check.mass && cell.mass / 5 < check.mass && - (!split) && this.splitCooldown == 0 && this.cells.length < 3) { - - var endDist = Math.max(this.splitDistance(cell), cell.getSize() * 4); + if (check.cellType == 0 && + cell.getSize() > (check.getSize() + 4) * 1.15 && + cell.getSize() < check.getSize() * 5 && + (!split) && + this.splitCooldown == 0 && + this.cells.length < 3) { + + var endDist = 780 + 40 - cell.getSize() / 2 - check.getSize(); - if (distance < endDist - cell.getSize() - check.getSize()) { + if (endDist > 0 && distance < endDist) { splitTarget = check; split = true; } @@ -141,22 +161,23 @@ BotPlayer.prototype.decide = function(cell) { result.add(force); } } - + // Normalize the resulting vector result.normalize(); - + // Check for splitkilling and threats if (split) { // Can be shortened but I'm too lazy if (threats.length > 0) { - if (this.largest(threats).mass / 2.6 > cell.mass) { // ??? but works + if (this.largest(threats).getSize() > cell.getSize() * 1.5) { // Splitkill the target this.mouse = { x: splitTarget.position.x, y: splitTarget.position.y }; this.splitCooldown = 16; - this.gameServer.splitCells(this); + this.socket.packetHandler.pressSpace = true; + //this.gameServer.splitCells(this); return; } } @@ -167,7 +188,8 @@ BotPlayer.prototype.decide = function(cell) { y: splitTarget.position.y }; this.splitCooldown = 16; - this.gameServer.splitCells(this); + this.socket.packetHandler.pressSpace = true; + //this.gameServer.splitCells(this); return; } } @@ -177,25 +199,15 @@ BotPlayer.prototype.decide = function(cell) { }; }; + // Subfunctions -BotPlayer.prototype.largest = function(list) { +BotPlayer.prototype.largest = function (list) { // Sort the cells by Array.sort() function to avoid errors var sorted = list.valueOf(); - sorted.sort(function(a, b) { - return b.mass - a.mass; + sorted.sort(function (a, b) { + return b.getSize() - a.getSize(); }); - - return sorted[0]; -}; - -BotPlayer.prototype.splitDistance = function(cell) { - // Calculate split distance and check if it is larger than the raw distance - var mass = cell.mass; - var t = Math.PI * Math.PI; - var modifier = 3 + Math.log(1 + mass) / 10; - var splitSpeed = cell.owner.gameServer.config.playerSpeed * Math.min(Math.pow(mass, -Math.PI / t / 10) * modifier, 150); - var endDist = Math.max(splitSpeed * 12.8, cell.getSize() * 2); // Checked via C#, final distance is near 6.512x splitSpeed - return endDist; + return sorted[0]; }; diff --git a/src/ai/FakeSocket.js b/src/ai/FakeSocket.js index 670d32f26..6204aee5d 100644 --- a/src/ai/FakeSocket.js +++ b/src/ai/FakeSocket.js @@ -1,33 +1,19 @@ -// A fake socket for bot players +// A fake socket for bot players function FakeSocket(gameServer) { this.server = gameServer; + this.isCloseRequest = false; } module.exports = FakeSocket; // Override -FakeSocket.prototype.sendPacket = function(packet) { +FakeSocket.prototype.sendPacket = function (packet) { // Fakes sending a packet return; }; -FakeSocket.prototype.close = function(error) { - // Removes the bot - var len = this.playerTracker.cells.length; - for (var i = 0; i < len; i++) { - var cell = this.playerTracker.cells[0]; - - if (!cell) { - continue; - } - - this.server.removeNode(cell); - } - - var index = this.server.clients.indexOf(this); - if (index != -1) { - this.server.clients.splice(index, 1); - } +FakeSocket.prototype.close = function (error) { + this.isCloseRequest = true; }; diff --git a/src/ai/Readme.txt b/src/ai/Readme.txt index 17f605ef7..c2660acf7 100644 --- a/src/ai/Readme.txt +++ b/src/ai/Readme.txt @@ -1,4 +1,4 @@ -[Ogar player bots] +[Ogar player bots] These bots are designed to be used for testing new commits of Ogar. To install this module, set the serverBots config field in gameserver.js to an amount higher than 0 (10 is a good amount), or issue command addBots [number] in console. diff --git a/src/badwords.txt b/src/badwords.txt new file mode 100644 index 000000000..56fb10212 --- /dev/null +++ b/src/badwords.txt @@ -0,0 +1,25 @@ +bitch +slut +cunt +dick +fuck +stupid +asshole +nigger +skank +whore +chink +fatass +shemale +gook +kraut +lesbo +lardass +biatch +douchebag +faggot +wetback +wop +shit +idiot +pedo diff --git a/src/entity/Cell.js b/src/entity/Cell.js index 738362322..22e5a9357 100644 --- a/src/entity/Cell.js +++ b/src/entity/Cell.js @@ -1,277 +1,288 @@ -function Cell(nodeId, owner, position, mass, gameServer) { - this.nodeId = nodeId; - this.owner = owner; // playerTracker that owns this cell - this.ticksLeft = 0; // Individual updates - this.color = { - r: 0, - g: 255, - b: 0 - }; - this.position = position; - this.mass = mass; // Starting mass of the cell - this.cellType = -1; // 0 = Player Cell, 1 = Food, 2 = Virus, 3 = Ejected Mass - this.spiked = 0; // If 1, then this cell has spikes around it - - this.killedBy; // Cell that ate this cell - this.gameServer = gameServer; - - this.moveEngineSpeed = 0; - this.moveDecay = 0.85; - this.angle = 0; // Angle of movement - this.collisionRestoreTicks = 0; // Ticks left before cell starts checking for collision with client's cells -} - -module.exports = Cell; - -// Fields not defined by the constructor are considered private and need a getter/setter to access from a different class - -Cell.prototype.getName = function() { - if (this.owner) { - return this.owner.name; - } else { - return ""; - } -}; - -Cell.prototype.setColor = function(color) { - this.color.r = color.r; - this.color.g = color.g; - this.color.b = color.b; -}; - -Cell.prototype.getColor = function() { - return this.color; -}; - -Cell.prototype.getType = function() { - return this.cellType; -}; - -Cell.prototype.getSize = function() { - // Calculates radius based on cell mass - return Math.ceil(Math.sqrt(100 * this.mass)); -}; - -Cell.prototype.getSquareSize = function() { - // R * R - return (100 * this.mass) >> 0; -}; - -Cell.prototype.addMass = function(n) { - // Check if the cell needs to autosplit before adding mass - if (this.mass > this.gameServer.config.playerMaxMass && this.owner.cells.length < this.gameServer.config.playerMaxCells) { - var splitMass = this.mass / 2; - var randomAngle = Math.random() * 6.28; // Get random angle - this.gameServer.createPlayerCell(this.owner, this, randomAngle, splitMass); - } else { - this.mass = Math.min(this.mass, this.gameServer.config.playerMaxMass); - } - this.mass += n; -}; - -Cell.prototype.getSpeed = function() { - // Based on 50ms ticks. If updateMoveEngine interval changes, change 50 to new value - // (should possibly have a config value for this?) - - // Old formulas: - // return 5 + (20 * (1 - (this.mass/(70+this.mass)))); - // return this.gameServer.config.playerSpeed * Math.pow(this.mass, -0.22) * 50 / 40; - return this.gameServer.config.playerSpeed * Math.pow(this.mass, -Math.PI / (Math.PI * Math.PI) / 1.5); -}; - -Cell.prototype.setAngle = function(radians) { - this.angle = radians; -}; - -Cell.prototype.getAngle = function() { - return this.angle; -}; - -Cell.prototype.setMoveEngineData = function(speed, decay) { - this.moveEngineSpeed = speed; - this.moveDecay = isNaN(decay) ? 0.75 : decay; -}; - -Cell.prototype.getEatingRange = function() { - return 0; // 0 for ejected cells -}; - -Cell.prototype.getKiller = function() { - return this.killedBy; -}; - -Cell.prototype.setKiller = function(cell) { - this.killedBy = cell; -}; - -// Functions - -Cell.prototype.collisionCheck = function(bottomY, topY, rightX, leftX) { - // Collision checking - if (this.position.y > bottomY) { - return false; - } - - if (this.position.y < topY) { - return false; - } - - if (this.position.x > rightX) { - return false; - } - - if (this.position.x < leftX) { - return false; - } - - return true; -}; - -// This collision checking function is based on CIRCLE shape -Cell.prototype.collisionCheck2 = function(objectSquareSize, objectPosition) { - // IF (O1O2 + r <= R) THEN collided. (O1O2: distance b/w 2 centers of cells) - // (O1O2 + r)^2 <= R^2 - // approximately, remove 2*O1O2*r because it requires sqrt(): O1O2^2 + r^2 <= R^2 - - var dx = this.position.x - objectPosition.x; - var dy = this.position.y - objectPosition.y; - - return (dx * dx + dy * dy + this.getSquareSize() <= objectSquareSize); -}; - -Cell.prototype.visibleCheck = function(box, centerPos, cells) { - // Checks if this cell is visible to the player - var isThere = false; - if (this.mass < 100) isThere = this.collisionCheck(box.bottomY, box.topY, box.rightX, box.leftX); - else { - var cellSize = this.getSize(); - var lenX = cellSize + box.width >> 0; // Width of cell + width of the box (Int) - var lenY = cellSize + box.height >> 0; // Height of cell + height of the box (Int) - - isThere = (this.abs(this.position.x - centerPos.x) < lenX) && (this.abs(this.position.y - centerPos.y) < lenY); - } - if (isThere) { - // It is - // To save perfomance, check if any client's cell collides with this cell - for (var i = 0; i < cells.length; i++) { - var cell = cells[i]; - if (!cell) continue; - - var xs = this.position.x - cell.position.x; - var ys = this.position.y - cell.position.y; - var sqDist = xs * xs + ys * ys; - - var collideDist = cell.getSquareSize() + this.getSquareSize(); - - if (sqDist < collideDist) { - return 2; - }// Colliding with one - } - return 1; // Not colliding with any - } - else return 0; -}; - -Cell.prototype.calcMovePhys = function(config) { - // Move, twice as slower - var X = this.position.x + ((this.moveEngineSpeed / 2) * Math.sin(this.angle)); - var Y = this.position.y + ((this.moveEngineSpeed / 2) * Math.cos(this.angle)); - - // Movement engine - var speedDecrease = this.moveEngineSpeed - this.moveEngineSpeed * this.moveDecay; - this.moveEngineSpeed -= speedDecrease / 2; // Decaying speed twice as slower - - // Ejected cell collision - if (this.cellType == 3) { - for (var i = 0; i < this.gameServer.nodesEjected.length; i++) { - var check = this.gameServer.nodesEjected[i]; - - if (check.nodeId == this.nodeId) continue; // Don't check for yourself - - var dist = this.getDist(this.position.x, this.position.y, check.position.x, check.position.y); - var allowDist = this.getSize() + check.getSize(); // Allow cells to get in themselves a bit - - if (dist < allowDist) { - // Two ejected cells collided - var deltaX = this.position.x - check.position.x; - var deltaY = this.position.y - check.position.y; - var angle = Math.atan2(deltaX, deltaY); - - var move = allowDist - dist; - - X += Math.sin(angle) * move / 2; - Y += Math.cos(angle) * move / 2; - } - } - } - - // Border check - Bouncy physics - var radius = 40; - if ((this.position.x - radius) < -config.borderLeft) { - // Flip angle horizontally - Left side - this.angle = 6.28 - this.angle; - X = -config.borderLeft + radius; - } - if ((this.position.x + radius) > config.borderRight) { - // Flip angle horizontally - Right side - this.angle = 6.28 - this.angle; - X = config.borderRight - radius; - } - if ((this.position.y - radius) < -config.borderTop) { - // Flip angle vertically - Top side - this.angle = (this.angle <= 3.14) ? 3.14 - this.angle : 9.42 - this.angle; - Y = -config.borderTop + radius; - } - if ((this.position.y + radius) > config.borderBottom) { - // Flip angle vertically - Bottom side - this.angle = (this.angle <= 3.14) ? 3.14 - this.angle : 9.42 - this.angle; - Y = config.borderBottom - radius; - } - - // Set position - this.position.x = X; - this.position.y = Y; -}; - -// Override these - -Cell.prototype.sendUpdate = function() { - // Whether or not to include this cell in the update packet - return true; -}; - -Cell.prototype.onConsume = function(consumer, gameServer) { - // Called when the cell is consumed -}; - -Cell.prototype.onAdd = function(gameServer) { - // Called when this cell is added to the world -}; - -Cell.prototype.onRemove = function(gameServer) { - // Called when this cell is removed -}; - -Cell.prototype.onAutoMove = function(gameServer) { - // Called on each auto move engine tick -}; - -Cell.prototype.moveDone = function(gameServer) { - // Called when this cell finished moving with the auto move engine -}; - -// Lib - -Cell.prototype.abs = function(x) { - return x < 0 ? -x : x; -}; - -Cell.prototype.getDist = function(x1, y1, x2, y2) { - var xs = x2 - x1; - xs = xs * xs; - - var ys = y2 - y1; - ys = ys * ys; - - return Math.sqrt(xs + ys); -}; +function Cell(gameServer, owner, position, size) { + this.gameServer = gameServer; + this.owner = owner; // playerTracker that owns this cell + + this.tickOfBirth = 0; + this.color = { r: 0, g: 0, b: 0 }; + this.position = { x: 0, y: 0 }; + this._size = null; + this._sizeSquared = null; + this._mass = null; + this._speed = null; + this.cellType = -1; // 0 = Player Cell, 1 = Food, 2 = Virus, 3 = Ejected Mass + this.isSpiked = false; // If true, then this cell has spikes around it + this.isAgitated = false;// If true, then this cell has waves on it's outline + this.killedBy = null; // Cell that ate this cell + this.isMoving = false; // Indicate that cell is in boosted mode + + this.boostDistance = 0; + this.boostDirection = { x: 1, y: 0, angle: Math.PI / 2 }; + this.boostMaxSpeed = 78; // boost speed limit, sqrt(780*780/100) + this.ejector = null; + + if (this.gameServer != null) { + this.nodeId = this.gameServer.getNextNodeId(); + this.tickOfBirth = this.gameServer.getTick(); + if (size != null) { + this.setSize(size); + } + if (position != null) { + this.setPosition(position); + } + } +} + +module.exports = Cell; + + +// Fields not defined by the constructor are considered private and need a getter/setter to access from a different class + +Cell.prototype.setColor = function (color) { + this.color.r = color.r; + this.color.g = color.g; + this.color.b = color.b; +}; + +Cell.prototype.getColor = function () { + return this.color; +}; + +Cell.prototype.getType = function () { + return this.cellType; +}; + +Cell.prototype.setSize = function (size) { + if (isNaN(size)) { + throw new TypeError("Cell.setSize: size is NaN"); + } + if (this._size === size) return; + this._size = size; + this._sizeSquared = size * size; + this._mass = null; + this._speed = null; + if (this.owner) + this.owner.massChanged(); +}; + +Cell.prototype.getSize = function () { + return this._size; +}; + +Cell.prototype.getSizeSquared = function () { + return this._sizeSquared; +}; + +Cell.prototype.getMass = function () { + if (this._mass == null) { + this._mass = this.getSizeSquared() / 100; + } + return this._mass; +}; + +Cell.prototype.getSpeed = function () { + if (this._speed == null) { + var speed = 2.1106 / Math.pow(this.getSize(), 0.449); + // tickStep=40ms + this._speed = speed * 40 * this.gameServer.config.playerSpeed; + } + return this._speed; +}; + +Cell.prototype.setAngle = function (angle) { + this.boostDirection = { + x: Math.sin(angle), + y: Math.cos(angle), + angle: angle + }; +}; + +Cell.prototype.getAngle = function () { + return this.boostDirection.angle; +}; + +// Returns cell age in ticks for specified game tick +Cell.prototype.getAge = function (tick) { + if (this.tickOfBirth == null) return 0; + return Math.max(0, tick - this.tickOfBirth); +} + +Cell.prototype.setKiller = function (cell) { + this.killedBy = cell; +}; + +Cell.prototype.getKiller = function () { + return this.killedBy; +}; + +Cell.prototype.setPosition = function (pos) { + if (pos == null || isNaN(pos.x) || isNaN(pos.y)) { + throw new TypeError("Cell.setPosition: position is NaN"); + } + this.position.x = pos.x; + this.position.y = pos.y; +}; + +// Virtual + +Cell.prototype.canEat = function (cell) { + // by default cell cannot eat anyone + return false; +}; + +Cell.prototype.onEat = function (prey) { + // Called to eat prey cell + this.setSize(Math.sqrt(this.getSizeSquared() + prey.getSizeSquared())); +}; + +Cell.prototype.onEaten = function (hunter) { +}; + +Cell.prototype.onAdd = function (gameServer) { + // Called when this cell is added to the world +}; + +Cell.prototype.onRemove = function (gameServer) { + // Called when this cell is removed +}; + +// Functions + +// Note: maxSpeed > 78 may leads to bug when cell can fly +// through other cell due to high speed +Cell.prototype.setBoost = function (distance, angle, maxSpeed) { + if (isNaN(angle)) angle = Math.PI / 2; + if (!maxSpeed) maxSpeed = 78; + + this.boostDistance = distance; + this.boostMaxSpeed = maxSpeed; + this.setAngle(angle); + this.isMoving = true; + if (!this.owner) { + var index = this.gameServer.movingNodes.indexOf(this); + if (index < 0) + this.gameServer.movingNodes.push(this); + } +}; + +Cell.prototype.move = function (border) { + if (this.isMoving && this.boostDistance <= 0) { + this.boostDistance = 0; + this.isMoving = false; + return; + } + var speed = Math.sqrt(this.boostDistance * this.boostDistance / 100); + var speed = Math.min(speed, this.boostMaxSpeed);// limit max speed with sqrt(780*780/100) + speed = Math.min(speed, this.boostDistance); // avoid overlap 0 + this.boostDistance -= speed; + if (this.boostDistance < 1) this.boostDistance = 0; + + var v = this.clipVelocity( + { x: this.boostDirection.x * speed, y: this.boostDirection.y * speed }, + border); + this.position.x += v.x; + this.position.y += v.y; + this.checkBorder(border); +} + +Cell.prototype.clipVelocity = function (v, border) { + if (isNaN(v.x) || isNaN(v.y)) { + throw new TypeError("Cell.clipVelocity: NaN"); + }; + if (v.x == 0 && v.y == 0) + return v; // zero move, no calculations :) + var r = this.getSize() / 2; + var bound = { + minx: border.minx + r, + miny: border.miny + r, + maxx: border.maxx - r, + maxy: border.maxy - r + }; + var x = this.position.x + v.x; + var y = this.position.y + v.y; + // border check + var pleft = x >= bound.minx ? null : findLineIntersection( + this.position.x, this.position.y, x, y, + bound.minx, bound.miny, bound.minx, bound.maxy); + var pright = x <= bound.maxx ? null : findLineIntersection( + this.position.x, this.position.y, x, y, + bound.maxx, bound.miny, bound.maxx, bound.maxy); + var ptop = y >= bound.miny ? null : findLineIntersection( + this.position.x, this.position.y, x, y, + bound.minx, bound.miny, bound.maxx, bound.miny); + var pbottom = y <= bound.maxy ? null : findLineIntersection( + this.position.x, this.position.y, x, y, + bound.minx, bound.maxy, bound.maxx, bound.maxy); + var ph = pleft != null ? pleft : pright; + var pv = ptop != null ? ptop : pbottom; + var p = ph != null ? ph : pv; + if (p == null) { + // inside border + return v; + } + if (ph && pv) { + // two border lines intersection => get nearest point + var hdx = ph.x - this.position.x; + var hdy = ph.y - this.position.y; + var vdx = pv.x - this.position.x; + var vdy = pv.y - this.position.y; + if (hdx * hdx + hdy * hdy < vdx * vdx + vdy * vdy) + p = ph; + else + p = pv; + } + // p - stop point on the border + + // reflect angle + var angle = this.getAngle(); + if (p == ph) { + // left/right border reflection + angle = 2 * Math.PI - angle; + } else { + // top/bottom border reflection + angle = angle <= Math.PI ? Math.PI - angle : 3 * Math.PI - angle; + } + this.setAngle(angle); + // new velocity + var lx = p.x - this.position.x; + var ly = p.y - this.position.y; + // calculate rest of velocity + var ldx = v.x - lx; + var ldy = v.y - ly; + // update velocity and add rest to the boostDistance + v.x = lx; + v.y = ly; + this.boostDistance += Math.sqrt(ldx * ldx + ldy * ldy); + if (this.boostDistance < 1) this.boostDistance = 0; + this.isMoving = true; + return v; +}; + +Cell.prototype.checkBorder = function (border) { + var r = this.getSize() / 2; + var x = this.position.x; + var y = this.position.y; + x = Math.max(x, border.minx + r); + y = Math.max(y, border.miny + r); + x = Math.min(x, border.maxx - r); + y = Math.min(y, border.maxy - r); + if (x != this.position.x || y != this.position.y) { + this.setPosition({ x: x, y: y }); + } +}; + + +// Lib + +function findLineIntersection(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + var z1 = p1x - p0x; + var z2 = p3x - p2x; + var w1 = p1y - p0y; + var w2 = p3y - p2y; + var k1 = w1 * z2 - z1 * w2; + if (k1 == 0) return null; + var k2 = (z1 * (p2y - p0y) + w1 * (p0x - p2x)) / k1; + var px = p2x + z2 * k2; + var py = p2y + w2 * k2; + if (isNaN(px) || isNaN(py)) return null; + return { x: px, y: py }; +} diff --git a/src/entity/EjectedMass.js b/src/entity/EjectedMass.js index e28468574..b5b480265 100644 --- a/src/entity/EjectedMass.js +++ b/src/entity/EjectedMass.js @@ -1,89 +1,25 @@ -var Cell = require('./Cell'); - -function EjectedMass() { - Cell.apply(this, Array.prototype.slice.call(arguments)); - - this.cellType = 3; - this.size = Math.ceil(Math.sqrt(100 * this.mass)); - this.squareSize = (100 * this.mass) >> 0; // not being decayed -> calculate one time - this.addedAntiTeam = false; // Not to affect anti-teaming two times -} - -module.exports = EjectedMass; -EjectedMass.prototype = new Cell(); - -// Override functions that use 'owner' variable -EjectedMass.prototype.getName = function() { - return ""; -}; - -EjectedMass.prototype.addMass = function(n) { - return; // Do nothing, this is an ejected cell -}; - - -// Cell-specific functions -EjectedMass.prototype.getSize = function() { - return this.size; -}; - -EjectedMass.prototype.getSquareSize = function() { - return this.squareSize; -}; - -EjectedMass.prototype.calcMove = null; // Only for player controlled movement - -// Main Functions - -EjectedMass.prototype.sendUpdate = function() { - // Whether or not to include this cell in the update packet - // Always true since ejected cells can collide with themselves - return true; -}; - -EjectedMass.prototype.onRemove = function(gameServer) { - // Check for teaming and apply anti-teaming if required - if (!this.addedAntiTeam && this.owner.checkForWMult) { - try { - if (this.gameServer.gameMode.teamAmount > 0) { - // Apply teaming EXCEPT when exchanging mass to same team member - if (this.owner.team != this.killedBy.owner.team || this.owner == this.killedBy.owner) { - this.owner.Wmult += 0.02; - this.owner.checkForWMult = false; - }; - } else { - // Always apply anti-teaming if there are no teams - this.owner.Wmult += 0.02; - this.owner.checkForWMult = false; - }; - } catch(ex) { } // Dont do anything whatever the error is - } - // Remove from list of ejected mass - var index = gameServer.nodesEjected.indexOf(this); - if (index != -1) { - gameServer.nodesEjected.splice(index, 1); - } -}; - -EjectedMass.prototype.onConsume = function(consumer, gameServer) { - // Adds mass to consumer - consumer.addMass(this.mass); -}; - -EjectedMass.prototype.onAutoMove = function(gameServer) { - if (gameServer.nodesVirus.length < gameServer.config.virusMaxAmount) { - // Check for viruses - var v = gameServer.getNearestVirus(this); - if (v) { // Feeds the virus if it exists - v.feed(this, gameServer); - return true; - } - } -}; - -EjectedMass.prototype.moveDone = function(gameServer) { - // Always apply anti-teaming - this.owner.actionMult += 0.02; - this.addedAntiTeam = true; - this.owner.checkForWMult = false; -}; +var Cell = require('./Cell'); + +function EjectedMass() { + Cell.apply(this, Array.prototype.slice.call(arguments)); + + this.cellType = 3; +} + +module.exports = EjectedMass; +EjectedMass.prototype = new Cell(); + +// Main Functions + +EjectedMass.prototype.onAdd = function (gameServer) { + // Add to list of ejected mass + gameServer.nodesEjected.push(this); +}; + +EjectedMass.prototype.onRemove = function (gameServer) { + // Remove from list of ejected mass + var index = gameServer.nodesEjected.indexOf(this); + if (index != -1) { + gameServer.nodesEjected.splice(index, 1); + } +}; diff --git a/src/entity/Food.js b/src/entity/Food.js index fd8d4a4a2..16fe7e14b 100644 --- a/src/entity/Food.js +++ b/src/entity/Food.js @@ -1,63 +1,20 @@ -var Cell = require('./Cell'); +var Cell = require('./Cell'); function Food() { Cell.apply(this, Array.prototype.slice.call(arguments)); - + this.cellType = 1; - this.size = Math.ceil(Math.sqrt(100 * this.mass)); - this.squareSize = (100 * this.mass) >> 0; // not being decayed -> calculate one time - this.shouldSendUpdate = false; - - if (this.gameServer.config.foodMassGrow && - this.gameServer.config.foodMassGrowPossiblity > Math.floor(Math.random() * 101)) { - this.grow(); - } } module.exports = Food; Food.prototype = new Cell(); -Food.prototype.getSize = function() { - return this.size; -}; - -Food.prototype.getSquareSize = function() { - return this.squareSize; -}; - -Food.prototype.calcMove = null; // Food has no need to move - // Main Functions -Food.prototype.grow = function() { - setTimeout(function() { - this.mass++; // food mass increased, we need to recalculate its size and squareSize, and send update to client side - this.size = Math.ceil(Math.sqrt(100 * this.mass)); - this.squareSize = (100 * this.mass) >> 0; - this.shouldSendUpdate = true; - - if (this.mass < this.gameServer.config.foodMassLimit) { - this.grow(); - } - }.bind(this), this.gameServer.config.foodMassTimeout * 1000); -}; - -Food.prototype.sendUpdate = function() { - // Whether or not to include this cell in the update packet - if (this.moveEngineTicks == 0) { - return false; - } - if (this.shouldSendUpdate) { - this.shouldSendUpdate = false; - return true; - } - return true; +Food.prototype.onAdd = function (gameServer) { + gameServer.currentFood++; }; -Food.prototype.onRemove = function(gameServer) { +Food.prototype.onRemove = function (gameServer) { gameServer.currentFood--; }; - -Food.prototype.onConsume = function(consumer, gameServer) { - consumer.addMass(this.mass); -}; diff --git a/src/entity/MotherCell.js b/src/entity/MotherCell.js new file mode 100644 index 000000000..d8791612b --- /dev/null +++ b/src/entity/MotherCell.js @@ -0,0 +1,73 @@ +var Cell = require('./Cell'); +var Food = require('./Food'); +var Virus = require('./Virus'); + +function MotherCell() { + Cell.apply(this, Array.prototype.slice.call(arguments)); + + this.cellType = 2; + this.isSpiked = true; + this.isMotherCell = true; // Not to confuse bots + this.setColor({ r: 0xce, g: 0x63, b: 0x63 }); + this.motherCellMinSize = 149; // vanilla 149 (mass = 149*149/100 = 222.01) + this.motherCellSpawnAmount = 2; + if (!this.getSize()) { + this.setSize(this.motherCellMinSize); + } +} + +module.exports = MotherCell; +MotherCell.prototype = new Cell(); + +// Main Functions + +MotherCell.prototype.canEat = function (cell) { + return cell.cellType == 0 || // can eat player cell + cell.cellType == 3; // can eat ejected mass +}; + +MotherCell.prototype.onEaten = Virus.prototype.onEaten; // Copies the virus prototype function + +MotherCell.prototype.onUpdate = function () { + if (this.getSize() <= this.motherCellMinSize) { + return; + } + var maxFood = this.gameServer.config.foodMaxAmount; + if (this.gameServer.currentFood >= maxFood) { + return; + } + var size1 = this.getSize(); + var size2 = this.gameServer.config.foodMinSize; + for (var i = 0; i < this.motherCellSpawnAmount; i++) { + size1 = Math.sqrt(size1 * size1 - size2 * size2); + size1 = Math.max(size1, this.motherCellMinSize); + this.setSize(size1); + + // Spawn food with size2 + var angle = Math.random() * 2 * Math.PI; + var r = this.getSize(); + var pos = { + x: this.position.x + r * Math.sin(angle), + y: this.position.y + r * Math.cos(angle) + }; + + // Spawn food + var food = new Food(this.gameServer, null, pos, size2); + food.setColor(this.gameServer.getRandomColor()); + this.gameServer.addNode(food); + + // Eject to random distance + food.setBoost(32 + 32 * Math.random(), angle); + + if (this.gameServer.currentFood >= maxFood || size1 <= this.motherCellMinSize) { + break; + } + } + this.gameServer.updateNodeQuad(this); +}; + +MotherCell.prototype.onAdd = function () { +}; + +MotherCell.prototype.onRemove = function () { +}; diff --git a/src/entity/PlayerCell.js b/src/entity/PlayerCell.js index 7653e7e73..0b86a6e1f 100644 --- a/src/entity/PlayerCell.js +++ b/src/entity/PlayerCell.js @@ -1,158 +1,103 @@ -var Cell = require('./Cell'); - -function PlayerCell() { - Cell.apply(this, Array.prototype.slice.call(arguments)); - - this.cellType = 0; - this.recombineTicks = 0; // Ticks passed after the cell has split - this.shouldRecombine = false; // Should the cell combine. If true, collision with own cells happens -} - -module.exports = PlayerCell; -PlayerCell.prototype = new Cell(); - -// Main Functions - -PlayerCell.prototype.simpleCollide = function(check, d) { - // Simple collision check - var len = 2 * d >> 0; // Width of cell + width of the box (Int) - - return (this.abs(this.position.x - check.x) < len) && - (this.abs(this.position.y - check.y) < len); -}; - -PlayerCell.prototype.calcMergeTime = function(base) { - // Check for merging time - var r = false; - if (base == 0 || this.owner.mergeOverride) { - // Instant recombine in config or merge command was triggered for this client - r = true; - } else { - var rec = Math.floor(base + ((0.02 * this.mass))); // base seconds + 0.02% of mass - if (this.recombineTicks > rec) r = true; // Can combine with other cells - } - this.shouldRecombine = r; -}; - -// Movement - -PlayerCell.prototype.calcMove = function(x2, y2, gameServer) { - if (!this.owner.shouldMoveCells && this.owner.notMoved) return; // Mouse is in one place - - // Get angle of mouse - var deltaY = y2 - this.position.y; - var deltaX = x2 - this.position.x; - var angle = Math.atan2(deltaX, deltaY); - - if (isNaN(angle)) { - return; - } - - var dist = this.getDist(this.position.x, this.position.y, x2, y2); - var speed = Math.min(this.getSpeed(), dist) / 2; // Twice as slower - - // Move cell - this.position.x += Math.sin(angle) * speed; - this.position.y += Math.cos(angle) * speed; - this.owner.notMoved = false; -}; - -PlayerCell.prototype.collision = function(gameServer) { - var config = gameServer.config; - var r = this.getSize(); // Cell radius - - // Collision check for other cells - for (var i = 0; i < this.owner.cells.length; i++) { - var cell = this.owner.cells[i]; - - if (!cell) continue; // Error - if (this.nodeId == cell.nodeId) continue; - - if ((!cell.shouldRecombine) || (!this.shouldRecombine)) { - // Cannot recombine - Collision with your own cells - var calcInfo = gameServer.checkCellCollision(this, cell); // Calculation info - - // Further calculations - if (calcInfo.collided) { // Collided - // Cell with collision restore ticks on should not collide - if (this.collisionRestoreTicks > 0 || cell.collisionRestoreTicks > 0) continue; - - // Call gameserver's function to collide cells - gameServer.cellCollision(this, cell, calcInfo); - } - } - } - - gameServer.gameMode.onCellMove(this, gameServer); - - // Check to ensure we're not passing the world border (shouldn't get closer than a quarter of the cell's diameter) - if (this.position.x < -config.borderLeft + r / 2) { - this.position.x = -config.borderLeft + r / 2; - } - if (this.position.x > config.borderRight - r / 2) { - this.position.x = config.borderRight - r / 2; - } - if (this.position.y < -config.borderTop + r / 2) { - this.position.y = -config.borderTop + r / 2; - } - if (this.position.y > config.borderBottom - r / 2) { - this.position.y = config.borderBottom - r / 2; - } -}; - -// Override - -PlayerCell.prototype.getEatingRange = function() { - return this.getSize() / 3.14; -}; - -PlayerCell.prototype.onConsume = function(consumer, gameServer) { - // Add an inefficiency for eating other players' cells - var factor = ( consumer.owner === this.owner ? 1 : gameServer.config.playerMassAbsorbed ); - // Anti-bot measure - factor = (consumer.mass >= 625 && this.mass <= 17 && gameServer.config.playerBotGrowEnabled == 1) ? 0 : factor; - consumer.addMass(factor * this.mass); -}; - -PlayerCell.prototype.onAdd = function(gameServer) { - // Add to special player node list - gameServer.nodesPlayer.push(this); - // Gamemode actions - gameServer.gameMode.onCellAdd(this); -}; - -PlayerCell.prototype.onRemove = function(gameServer) { - var index; - // Remove from player cell list - index = this.owner.cells.indexOf(this); - if (index != -1) { - this.owner.cells.splice(index, 1); - } - // Remove from special player controlled node list - index = gameServer.nodesPlayer.indexOf(this); - if (index != -1) { - gameServer.nodesPlayer.splice(index, 1); - } - // Gamemode actions - gameServer.gameMode.onCellRemove(this); -}; - -PlayerCell.prototype.moveDone = function(gameServer) { - // Well, nothing. -}; - -// Lib - -PlayerCell.prototype.abs = function(x) { - return x < 0 ? -x : x; -}; - -PlayerCell.prototype.getDist = function(x1, y1, x2, y2) { - var xs = x2 - x1; - xs = xs * xs; - - var ys = y2 - y1; - ys = ys * ys; - - return Math.sqrt(xs + ys); -}; +var Cell = require('./Cell'); + +function PlayerCell() { + Cell.apply(this, Array.prototype.slice.call(arguments)); + + this.cellType = 0; + this._canRemerge = false; +} + +module.exports = PlayerCell; +PlayerCell.prototype = new Cell(); + +// Main Functions + +PlayerCell.prototype.updateRemerge = function () { + var age = this.getAge(this.gameServer.getTick()); + if (age < 15) { + // do not remerge if cell age is smaller than 15 ticks + this._canRemerge = false; + return; + } + var baseTtr = this.gameServer.config.playerRecombineTime; // default baseTtr = 30 + if (baseTtr == 0) { + // instant merge + if (this.getSize() >= 780 / 2) { + this._canRemerge = age > 20; + return; + } + this._canRemerge = this.boostDistance < 100; + return; + } + var ttr = Math.max(baseTtr, (this.getSize() * 0.2) >> 0); // ttr in seconds + // seconds to ticks (tickStep = 0.040 sec => 1 / 0.040 = 25) + ttr *= 25; + this._canRemerge = age >= ttr; +} + +PlayerCell.prototype.canRemerge = function () { + return this._canRemerge; +}; + +PlayerCell.prototype.canEat = function (cell) { + // player cell can eat anyone + return true; +}; + +PlayerCell.prototype.getSplitSize = function () { + return this.getSize() * splitMultiplier; +}; + +var splitMultiplier = 1 / Math.sqrt(2); + +// Movement + +PlayerCell.prototype.moveUser = function (border) { + if (this.owner == null || this.owner.socket.isConnected === false) { + return; + } + var x = this.owner.mouse.x; + var y = this.owner.mouse.y; + if (isNaN(x) || isNaN(y)) { + return; + } + var dx = ~~(x - this.position.x); + var dy = ~~(y - this.position.y); + var squared = dx * dx + dy * dy; + if (squared < 1) return; + + // distance + var d = Math.sqrt(squared); + + // normal + var invd = 1 / d; + var nx = dx * invd; + var ny = dy * invd; + + // normalized distance (0..1) + d = Math.min(d, 32) / 32; + var speed = this.getSpeed() * d; + if (speed <= 0) return; + + this.position.x += nx * speed; + this.position.y += ny * speed; + this.checkBorder(border); +}; + +// Override + +PlayerCell.prototype.onAdd = function (gameServer) { + // Gamemode actions + gameServer.gameMode.onCellAdd(this); +}; + +PlayerCell.prototype.onRemove = function (gameServer) { + var index; + // Remove from player cell list + index = this.owner.cells.indexOf(this); + if (index != -1) { + this.owner.cells.splice(index, 1); + } + // Gamemode actions + gameServer.gameMode.onCellRemove(this); +}; diff --git a/src/entity/Virus.js b/src/entity/Virus.js index 96072feba..dd790a855 100644 --- a/src/entity/Virus.js +++ b/src/entity/Virus.js @@ -1,108 +1,76 @@ -var Cell = require('./Cell'); - -function Virus() { - Cell.apply(this, Array.prototype.slice.call(arguments)); - - this.cellType = 2; - this.spiked = 1; - this.fed = 0; - this.isMotherCell = false; // Not to confuse bots -} - -module.exports = Virus; -Virus.prototype = new Cell(); - -Virus.prototype.calcMove = null; // Only for player controlled movement - -Virus.prototype.feed = function(feeder, gameServer) { - if (this.moveEngineSpeed <= 1) this.setAngle(feeder.getAngle()); // Set direction if the virus explodes - this.mass += feeder.mass; - this.fed++; // Increase feed count - feeder.setKiller(this); - gameServer.removeNode(feeder); - - // Check if the virus is going to explode - if (this.fed >= gameServer.config.virusFeedAmount) { - this.mass = gameServer.config.virusStartMass; // Reset mass - this.fed = 0; - gameServer.shootVirus(this); - } - -}; - -// Main Functions - -Virus.prototype.getEatingRange = function() { - return this.getSize() / 3.14; // 0 for ejected cells -}; - -Virus.prototype.onConsume = function(consumer, gameServer) { - var client = consumer.owner; - - // Cell consumes mass before any calculation - consumer.addMass(this.mass); - - var maxSplits = Math.floor(consumer.mass / 16) - 1; // Maximum amount of splits - var numSplits = gameServer.config.playerMaxCells - client.cells.length; // Get number of splits - numSplits = Math.min(numSplits, maxSplits); - var splitMass = Math.min(consumer.mass / (numSplits + 1), 24); // Maximum size of new splits - - // Cell cannot split any further - if (numSplits <= 0) { - return; - } - - var mass = consumer.mass; // Mass of the consumer - var bigSplits = []; // Big splits - - // Big cells will split into cells larger than 24 mass - // won't do the regular way unless it can split more than 4 times - if (numSplits == 1) bigSplits = [mass / 2]; - else if (numSplits == 2) bigSplits = [mass / 4, mass / 4]; - else if (numSplits == 3) bigSplits = [mass / 4, mass / 4, mass / 7]; - else if (numSplits == 4) bigSplits = [mass / 5, mass / 7, mass / 8, mass / 10]; - else { - var endMass = mass - numSplits * splitMass; - var m = endMass, - i = 0; - if (m > 466) { // Threshold - // While can split into an even smaller cell (1000 => 333, 167, etc) - var mult = 3.33; - while (m / mult > 24) { - m /= mult; - mult = 2; // First mult 3.33, the next ones 2 - bigSplits.push(m >> 0); - i++; - } - } - } - numSplits -= bigSplits.length; - - for (var k = 0; k < bigSplits.length; k++) { - var angle = Math.random() * 6.28; // Random directions - gameServer.createPlayerCell(client, consumer, angle, bigSplits[k]); - } - - // Splitting - for (var k = 0; k < numSplits; k++) { - var angle = Math.random() * 6.28; // Random directions - gameServer.createPlayerCell(client, consumer, angle, splitMass); - } - - // Prevent consumer cell from merging with other cells - consumer.calcMergeTime(gameServer.config.playerRecombineTime); - client.applyTeaming(1.2, 1); // Apply anti-teaming -}; - -Virus.prototype.onAdd = function(gameServer) { - gameServer.nodesVirus.push(this); -}; - -Virus.prototype.onRemove = function(gameServer) { - var index = gameServer.nodesVirus.indexOf(this); - if (index != -1) { - gameServer.nodesVirus.splice(index, 1); - } else { - console.log("[Warning] Tried to remove a non existing virus!"); - } -}; +var Cell = require('./Cell'); +var Logger = require('../modules/Logger'); + +function Virus() { + Cell.apply(this, Array.prototype.slice.call(arguments)); + + this.cellType = 2; + this.isSpiked = true; + this.fed = 0; + this.isMotherCell = false; // Not to confuse bots + this.setColor({ r: 0x33, g: 0xff, b: 0x33 }); +} + +module.exports = Virus; +Virus.prototype = new Cell(); + +// Main Functions + +Virus.prototype.canEat = function (cell) { + return cell.cellType == 3; // virus can eat ejected mass only +}; + +Virus.prototype.onEat = function (prey) { + // Called to eat prey cell + this.setSize(Math.sqrt(this.getSizeSquared() + prey.getSizeSquared())); + + if (this.getSize() >= this.gameServer.config.virusMaxSize) { + this.setSize(this.gameServer.config.virusMinSize); // Reset mass + this.gameServer.shootVirus(this, prey.getAngle()); + } +}; + +Virus.prototype.onEaten = function (consumer) { + var client = consumer.owner; + if (client == null) return; + + var maxSplit = this.gameServer.config.playerMaxCells - consumer.owner.cells.length; + var masses = this.gameServer.splitMass(consumer.getMass(), maxSplit + 1); + if (masses.length < 2) { + return; + } + + // Balance mass around center & skip first mass (==consumer mass) + var massesMix = []; + for (var i = 1; i < masses.length; i += 2) + massesMix.push(masses[i]); + for (var i = 2; i < masses.length; i += 2) + massesMix.push(masses[i]); + masses = massesMix; + + // Blow up the cell... + var angle = 2 * Math.PI * Math.random(); + var step = 2 * Math.PI / masses.length; + for (var i = 0; i < masses.length; i++) { + if (!this.gameServer.splitPlayerCell(client, consumer, angle, masses[i])) { + break; + } + angle += step; + if (angle >= 2 * Math.PI) { + angle -= 2 * Math.PI; + } + } +}; + +Virus.prototype.onAdd = function (gameServer) { + gameServer.nodesVirus.push(this); +}; + +Virus.prototype.onRemove = function (gameServer) { + var index = gameServer.nodesVirus.indexOf(this); + if (index != -1) { + gameServer.nodesVirus.splice(index, 1); + } else { + Logger.error("Virus.onRemove: Tried to remove a non existing virus!"); + } +}; diff --git a/src/entity/index.js b/src/entity/index.js index ae4ddedbb..745b68638 100644 --- a/src/entity/index.js +++ b/src/entity/index.js @@ -1,7 +1,8 @@ -module.exports = { +module.exports = { Cell: require('./Cell'), PlayerCell: require('./PlayerCell'), Food: require('./Food'), Virus: require('./Virus'), + MotherCell: require('./MotherCell'), EjectedMass: require('./EjectedMass'), }; diff --git a/src/enum/LogLevelEnum.js b/src/enum/LogLevelEnum.js new file mode 100644 index 000000000..c559b9be6 --- /dev/null +++ b/src/enum/LogLevelEnum.js @@ -0,0 +1,15 @@ +function define(name, value) { + Object.defineProperty(exports, name, { + value: value, + enumerable: true, + writable: false, + configurable: false + }); +} + +define("NONE", 0); +define("FATAL", 1); +define("ERROR", 2); +define("WARN", 3); +define("INFO", 4); +define("DEBUG", 5); diff --git a/src/enum/UserRoleEnum.js b/src/enum/UserRoleEnum.js new file mode 100644 index 000000000..51e802849 --- /dev/null +++ b/src/enum/UserRoleEnum.js @@ -0,0 +1,13 @@ +function define(name, value) { + Object.defineProperty(exports, name, { + value: value, + enumerable: true, + writable: false, + configurable: false + }); +} + +define("GUEST", 0); +define("USER", 1); +define("MODER", 2); +define("ADMIN", 4); diff --git a/src/gamemodes/Debug.js b/src/gamemodes/Debug.js deleted file mode 100644 index 57c5524ae..000000000 --- a/src/gamemodes/Debug.js +++ /dev/null @@ -1,60 +0,0 @@ -var FFA = require('./FFA'); // Base gamemode -var Packet = require('../packet'); - -function Debug() { - FFA.apply(this, Array.prototype.slice.call(arguments)); - - this.ID = 21; - this.name = "Debug Mode"; - this.specByLeaderboard = false; -} - -module.exports = Debug; -Debug.prototype = new FFA(); - -// Gamemode Specific Functions - -Debug.prototype.testPath = function(gameServer, player) { - var cell = player.cells[0]; - var check = gameServer.nodesVirus[0]; - - var v1 = Math.atan2(cell.position.x - player.mouse.x, cell.position.y - player.mouse.y); - - // Get angle of vector (cell -> virus) - var v2 = this.getAngle(cell, check); - var dist = this.getDist(cell, check); - console.log(v1); - console.log(v2); - - var inRange = Math.atan((2 * cell.getSize()) / dist); // Opposite/adjacent - console.log(inRange); - if ((v1 <= (v2 + inRange)) && (v1 >= (v2 - inRange))) { - console.log("Collided!"); - } -}; - -Debug.prototype.getAngle = function(c1, c2) { - var deltaY = c1.position.y - c2.position.y; - var deltaX = c1.position.x - c2.position.x; - return Math.atan2(deltaX, deltaY); -}; - -Debug.prototype.getDist = function(cell, check) { - // Fastest distance - I have a crappy computer to test with :( - var xd = (check.position.x - cell.position.x); - xd = xd < 0 ? xd * -1 : xd; // Math.abs is slow - - var yd = (check.position.y - cell.position.y); - yd = yd < 0 ? yd * -1 : yd; // Math.abs is slow - - return (xd + yd); -}; - -// Override - -Debug.prototype.pressW = function(gameServer, player) { - // Called when the Q key is pressed - console.log("Test:"); - this.testPath(gameServer, player); - player.socket.sendPacket(new Packet.DrawLine(player.mouse.x, player.mouse.y)); -}; diff --git a/src/gamemodes/Experimental.js b/src/gamemodes/Experimental.js index 9e073f9b7..4d9965d97 100644 --- a/src/gamemodes/Experimental.js +++ b/src/gamemodes/Experimental.js @@ -1,26 +1,24 @@ -var FFA = require('./FFA'); // Base gamemode -var Cell = require('../entity/Cell'); -var Food = require('../entity/Food'); -var Virus = require('../entity/Virus'); -var VirusFeed = require('../entity/Virus').prototype.feed; +var FFA = require('./FFA'); // Base gamemode +var Entity = require('../entity'); +var Logger = require('../modules/Logger'); function Experimental() { FFA.apply(this, Array.prototype.slice.call(arguments)); - + this.ID = 2; this.name = "Experimental"; this.specByLeaderboard = true; - + // Gamemode Specific Variables this.nodesMother = []; - this.tickMother = 0; - this.tickMotherS = 0; - + this.tickMotherSpawn = 0; + this.tickMotherUpdate = 0; + // Config - this.motherCellMass = 222; - this.motherUpdateInterval = 5; // How many ticks it takes to update the mother cell (1 tick = 50 ms) - this.motherSpawnInterval = 100; // How many ticks it takes to spawn another mother cell - Currently 5 seconds - this.motherMinAmount = 5; + this.motherSpawnInterval = 25 * 5; // How many ticks it takes to spawn another mother cell (5 seconds) + this.motherUpdateInterval = 5; // How many ticks it takes to spawn mother food (1 second) + this.motherMinAmount = 20; + this.motherMaxAmount = 30; } module.exports = Experimental; @@ -28,235 +26,83 @@ Experimental.prototype = new FFA(); // Gamemode Specific Functions -Experimental.prototype.updateMotherCells = function(gameServer) { - for (var i in this.nodesMother) { - var mother = this.nodesMother[i]; - - // Checks - mother.update(gameServer); - mother.checkEat(gameServer); - } -}; - -Experimental.prototype.spawnMotherCell = function(gameServer) { +Experimental.prototype.spawnMotherCell = function (gameServer) { // Checks if there are enough mother cells on the map - if (this.nodesMother.length < this.motherMinAmount) { - // Spawns a mother cell - var pos = gameServer.getRandomPosition(); - - // Check for players - for (var i = 0; i < gameServer.nodesPlayer.length; i++) { - var check = gameServer.nodesPlayer[i]; - - var r = check.getSize(); // Radius of checking player cell - - // Collision box - var topY = check.position.y - r; - var bottomY = check.position.y + r; - var leftX = check.position.x - r; - var rightX = check.position.x + r; - - // Check for collisions - if (pos.y > bottomY) { - continue; - } - - if (pos.y < topY) { - continue; - } - - if (pos.x > rightX) { - continue; - } - - if (pos.x < leftX) { - continue; - } - - // Collided - return; - } - - // Spawn if no cells are colliding - var m = new MotherCell(gameServer.getNextNodeId(), null, pos, this.motherCellMass); - gameServer.addNode(m); + if (this.nodesMother.length >= this.motherMinAmount) { + return; } + // Spawns a mother cell + var pos = gameServer.getRandomPosition(); + if (gameServer.willCollide(pos, 149)) { + // cannot find safe position => do not spawn + return; + } + // Spawn if no cells are colliding + var mother = new Entity.MotherCell(gameServer, null, pos, null); + gameServer.addNode(mother); }; // Override -Experimental.prototype.onServerInit = function(gameServer) { +Experimental.prototype.onServerInit = function (gameServer) { // Called when the server starts gameServer.run = true; - - var mapSize = gameServer.config.borderLeft + gameServer.config.borderRight + - gameServer.config.borderTop + gameServer.config.borderRight; - - this.motherMinAmount = Math.ceil(mapSize / 3194.382825); // 7 mother cells for agar.io map size - + + var mapSize = Math.max(gameServer.border.width, gameServer.border.height); + + // 7 mother cells for vanilla map size + //this.motherMinAmount = Math.ceil(mapSize / 2000); + //this.motherMaxAmount = this.motherMinAmount * 2; + + var self = this; + // Override + // Special virus mechanics - Virus.prototype.feed = function(feeder, gameServer) { - gameServer.removeNode(feeder); + Entity.Virus.prototype.onEat = function (prey) { // Pushes the virus - // Effect on angle is smaller with larger move engine speed - var angle = this.getAngle() + feeder.getAngle(), - effect = Math.sqrt(this.moveEngineSpeed + 1); - angle += feeder.getAngle() / effect; - angle /= (1 + effect); - this.setAngle(angle); // Set direction if the virus explodes - this.moveEngineSpeed += 10; - this.moveDecay = 0.9; + var angle = prey.isMoving ? prey.getAngle() : this.getAngle(); + this.setBoost(16 * 20, angle); + }; + Entity.MotherCell.prototype.onAdd = function () { + self.nodesMother.push(this); + }; + Entity.MotherCell.prototype.onRemove = function () { + var index = self.nodesMother.indexOf(this); + if (index != -1) { + self.nodesMother.splice(index, 1); + } else { + Logger.error("Experimental.onServerInit.MotherVirus.onRemove: Tried to remove a non existing virus!"); + } }; - - // Override this - gameServer.getRandomSpawn = gameServer.getRandomPosition; -}; - -Experimental.prototype.onTick = function(gameServer) { - // Mother Cell updates - this.updateMotherCells(gameServer); - - // Mother Cell Spawning - if (this.tickMotherS >= this.motherSpawnInterval) { - this.spawnMotherCell(gameServer); - this.tickMotherS = 0; - } else { - this.tickMotherS++; - } }; -Experimental.prototype.onChange = function(gameServer) { +Experimental.prototype.onChange = function (gameServer) { // Remove all mother cells for (var i in this.nodesMother) { gameServer.removeNode(this.nodesMother[i]); } + this.nodesMother = []; // Add back default functions - Virus.prototype.feed = VirusFeed; - gameServer.getRandomSpawn = require('../GameServer').prototype.getRandomSpawn; + Entity.Virus.prototype.onEat = require('../Entity/Virus').prototype.onEat; + Entity.MotherCell.prototype.onAdd = require('../Entity/MotherCell').prototype.onAdd; + Entity.MotherCell.prototype.onRemove = require('../Entity/MotherCell').prototype.onRemove; }; -// New cell type - -function MotherCell() { // Temporary - Will be in its own file if Zeach decides to add this to vanilla - Cell.apply(this, Array.prototype.slice.call(arguments)); - - this.cellType = 2; // Copies virus cell - this.color = { - r: 205, - g: 85, - b: 100 - }; - this.spiked = 1; - this.isMotherCell = true; // Not to confuse bots -} - -MotherCell.prototype = new Cell(); // Base - -MotherCell.prototype.getEatingRange = function() { - return this.getSize() / 3.14; -}; - -MotherCell.prototype.update = function(gameServer) { - if (Math.random() * 100 > 97) { - var maxFood = Math.random() * 2; // Max food spawned per tick - var i = 0; // Food spawn counter - while (i < maxFood) { - // Only spawn if food cap hasn't been reached - if (gameServer.currentFood < gameServer.config.foodMaxAmount * 1.5) { - this.spawnFood(gameServer); - } - - // Increment - i++; - } +Experimental.prototype.onTick = function (gameServer) { + // Mother Cell Spawning + if (this.tickMotherSpawn >= this.motherSpawnInterval) { + this.tickMotherSpawn = 0; + this.spawnMotherCell(gameServer); + } else { + this.tickMotherSpawn++; } - if (this.mass > 222) { - // Always spawn food if the mother cell is larger than 222 - var cellSize = gameServer.config.foodMass; - var remaining = this.mass - 222; - var maxAmount = Math.min(Math.floor(remaining / cellSize), 2); - for (var i = 0; i < maxAmount; i++) { - this.spawnFood(gameServer); - this.mass -= cellSize; + if (this.tickMotherUpdate >= this.motherUpdateInterval) { + this.tickMotherUpdate = 0; + for (var i = 0; i < this.nodesMother.length; i++) { + this.nodesMother[i].onUpdate(); } + } else { + this.tickMotherUpdate++; } }; -MotherCell.prototype.checkEat = function(gameServer) { - var safeMass = this.mass * .78; - - // Loop for potential prey - for (var i in gameServer.nodesPlayer) { - var check = gameServer.nodesPlayer[i]; - this.checkEatCell(check, safeMass, gameServer); - } - - // Viruses might be literally in the mother cell when it becomes large. Prevent this - for (var i in gameServer.nodesVirus) { - var check = gameServer.nodesVirus[i]; - this.checkEatCell(check, safeMass, gameServer); - } - - // Check ejected cells - for (var i in gameServer.nodesEjected) { - var check = gameServer.nodesEjected[i]; - this.checkEatCell(check, safeMass, gameServer); - } -}; - -MotherCell.prototype.checkEatCell = function(check, safeMass, gameServer) { - if ((check.getType() == 1) || (check.mass > safeMass)) { - // Too big to be consumed or check is a food cell - return; - } - - // Very simple yet very powerful - var dist = this.getDist(this.position.x, this.position.y, check.position.x, check.position.y); - var allowDist = this.getSize() - check.getEatingRange(); - if (dist < allowDist) { - // Eat it - check.setKiller(this); - gameServer.removeNode(check); - this.mass += check.mass; - } -}; - -MotherCell.prototype.abs = function(n) { - // Because Math.abs is slow - return (n < 0) ? -n : n; -}; - -MotherCell.prototype.spawnFood = function(gameServer) { - // Get starting position - var angle = Math.random() * 6.28; - var r = this.getSize(); - var pos = { - x: this.position.x + (r * Math.sin(angle)), - y: this.position.y + (r * Math.cos(angle)) - }; - - // Spawn food - var f = new Food(gameServer.getNextNodeId(), null, pos, gameServer.config.foodMass, gameServer); - f.setColor(gameServer.getRandomColor()); - - gameServer.addNode(f); - gameServer.currentFood++; - - // Move engine - f.angle = angle; - var dist = (Math.random() * 8) + 8; // Random distance - f.setMoveEngineData(dist, 0.9); -}; - -MotherCell.prototype.onConsume = Virus.prototype.onConsume; // Copies the virus prototype function - -MotherCell.prototype.onAdd = function(gameServer) { - gameServer.gameMode.nodesMother.push(this); // Temporary -}; - -MotherCell.prototype.onRemove = function(gameServer) { - var index = gameServer.gameMode.nodesMother.indexOf(this); - if (index != -1) { - gameServer.gameMode.nodesMother.splice(index, 1); - } -}; diff --git a/src/gamemodes/FFA.js b/src/gamemodes/FFA.js index 35250a2cf..59163afec 100644 --- a/src/gamemodes/FFA.js +++ b/src/gamemodes/FFA.js @@ -1,109 +1,75 @@ -var Mode = require('./Mode'); - -function FFA() { - Mode.apply(this, Array.prototype.slice.call(arguments)); - - this.ID = 0; - this.name = "Free For All"; - this.specByLeaderboard = true; -} - -module.exports = FFA; -FFA.prototype = new Mode(); - -// Gamemode Specific Functions - -FFA.prototype.leaderboardAddSort = function(player, leaderboard) { - // Adds the player and sorts the leaderboard - var len = leaderboard.length - 1; - var loop = true; - while ((len >= 0) && (loop)) { - // Start from the bottom of the leaderboard - if (player.getScore(false) <= leaderboard[len].getScore(false)) { - leaderboard.splice(len + 1, 0, player); - loop = false; // End the loop if a spot is found - } - len--; - } - if (loop) { - // Add to top of the list because no spots were found - leaderboard.splice(0, 0, player); - } -}; - -// Override - -FFA.prototype.onPlayerSpawn = function(gameServer, player) { - // Random color - player.color = gameServer.getRandomColor(); - - // Set up variables - var pos, startMass; - - // Check if there are ejected mass in the world. - if (gameServer.nodesEjected.length > 0) { - var index = Math.floor(Math.random() * 100) + 1; - if (index >= gameServer.config.ejectSpawnPlayer) { - // Get ejected cell - index = Math.floor(Math.random() * gameServer.nodesEjected.length); - var e = gameServer.nodesEjected[index]; - if (e.moveEngineTicks > 0) { - // Ejected cell is currently moving - gameServer.spawnPlayer(player, pos, startMass); - } - - // Remove ejected mass - gameServer.removeNode(e); - - // Inherit - pos = { - x: e.position.x, - y: e.position.y - }; - startMass = Math.max(e.mass, gameServer.config.playerStartMass); - - var color = e.getColor(); - player.setColor({ - 'r': color.r, - 'g': color.g, - 'b': color.b - }); - } - } - - // Spawn player - gameServer.spawnPlayer(player, pos, startMass); -}; - -FFA.prototype.updateLB = function(gameServer) { - var lb = gameServer.leaderboard; - // Loop through all clients - for (var i = 0; i < gameServer.clients.length; i++) { - if (typeof gameServer.clients[i] == "undefined") { - continue; - } - - var player = gameServer.clients[i].playerTracker; - if (player.disconnect > -1) continue; // Don't add disconnected players to list - var playerScore = player.getScore(true); - if (player.cells.length <= 0) { - continue; - } - - if (lb.length == 0) { - // Initial player - lb.push(player); - continue; - } else if (lb.length < gameServer.config.serverMaxLB) { - this.leaderboardAddSort(player, lb); - } else { - // 10 in leaderboard already - if (playerScore > lb[gameServer.config.serverMaxLB - 1].getScore(false)) { - lb.pop(); - this.leaderboardAddSort(player, lb); - } - } - } - - this.rankOne = lb[0]; -} +var Mode = require('./Mode'); + +function FFA() { + Mode.apply(this, Array.prototype.slice.call(arguments)); + + this.ID = 0; + this.name = "Free For All"; + this.specByLeaderboard = true; +} + +module.exports = FFA; +FFA.prototype = new Mode(); + +// Gamemode Specific Functions + +FFA.prototype.leaderboardAddSort = function (player, leaderboard) { + // Adds the player and sorts the leaderboard + var len = leaderboard.length - 1; + var loop = true; + while ((len >= 0) && (loop)) { + // Start from the bottom of the leaderboard + if (player.getScore() <= leaderboard[len].getScore()) { + leaderboard.splice(len + 1, 0, player); + loop = false; // End the loop if a spot is found + } + len--; + } + if (loop) { + // Add to top of the list because no spots were found + leaderboard.splice(0, 0, player); + } +}; + +// Override + +FFA.prototype.onPlayerSpawn = function (gameServer, player) { + player.setColor(player.isMinion ? { r: 240, g: 240, b: 255 } : gameServer.getRandomColor()); + // Spawn player + gameServer.spawnPlayer(player); +}; + +FFA.prototype.updateLB = function (gameServer) { + gameServer.leaderboardType = this.packetLB; + var lb = gameServer.leaderboard; + // Loop through all clients + for (var i = 0; i < gameServer.clients.length; i++) { + var client = gameServer.clients[i]; + if (client == null) continue; + + var player = client.playerTracker; + if (player.isRemoved) + continue; // Don't add disconnected players to list + + var playerScore = player.getScore(); + + if (player.cells.length <= 0) + continue; + + if (lb.length == 0) { + // Initial player + lb.push(player); + continue; + } else if (lb.length < gameServer.config.serverMaxLB) { + this.leaderboardAddSort(player, lb); + } else { + // 10 in leaderboard already + if (playerScore > lb[gameServer.config.serverMaxLB - 1].getScore()) { + lb.pop(); + this.leaderboardAddSort(player, lb); + } + } + } + + this.rankOne = lb[0]; +} diff --git a/src/gamemodes/HungerGames.js b/src/gamemodes/HungerGames.js index 32be736f7..8f11345d0 100644 --- a/src/gamemodes/HungerGames.js +++ b/src/gamemodes/HungerGames.js @@ -1,12 +1,12 @@ -var Tournament = require('./Tournament'); +var Tournament = require('./Tournament'); var Entity = require('../entity'); function HungerGames() { Tournament.apply(this, Array.prototype.slice.call(arguments)); - + this.ID = 11; this.name = "Hunger Games"; - + // Gamemode Specific Variables this.maxContenders = 12; this.baseSpawnPoints = [{ @@ -59,65 +59,61 @@ HungerGames.prototype = new Tournament(); // Gamemode Specific Functions -HungerGames.prototype.getPos = function() { +HungerGames.prototype.getPos = function () { var pos = { x: 0, y: 0 }; - + // Random Position if (this.contenderSpawnPoints.length > 0) { var index = Math.floor(Math.random() * this.contenderSpawnPoints.length); pos = this.contenderSpawnPoints[index]; this.contenderSpawnPoints.splice(index, 1); } - + return { x: pos.x, y: pos.y }; }; -HungerGames.prototype.spawnFood = function(gameServer, mass, pos) { - var f = new Entity.Food(gameServer.getNextNodeId(), null, pos, mass, gameServer); - f.setColor(gameServer.getRandomColor()); - - gameServer.addNode(f); - gameServer.currentFood++; +HungerGames.prototype.spawnFood = function (gameServer, mass, pos) { + var cell = new Entity.Food(gameServer, null, pos, mass); + cell.setColor(gameServer.getRandomColor()); + gameServer.addNode(cell); }; -HungerGames.prototype.spawnVirus = function(gameServer, pos) { - var v = new Entity.Virus(gameServer.getNextNodeId(), null, pos, gameServer.config.virusStartMass); +HungerGames.prototype.spawnVirus = function (gameServer, pos) { + var v = new Entity.Virus(gameServer, null, pos, gameServer.config.virusMinSize); gameServer.addNode(v); }; -HungerGames.prototype.onPlayerDeath = function(gameServer) { - var config = gameServer.config; - config.borderLeft += this.borderDec; - config.borderRight -= this.borderDec; - config.borderTop += this.borderDec; - config.borderBottom -= this.borderDec; - +HungerGames.prototype.onPlayerDeath = function (gameServer) { + gameServer.setBorder( + gameServer.border.width - this.borderDec * 2, + gameServer.border.height - this.borderDec * 2); + // Remove all cells var len = gameServer.nodes.length; for (var i = 0; i < len; i++) { var node = gameServer.nodes[i]; - + if ((!node) || (node.getType() == 0)) { continue; } - + // Move - if (node.position.x < config.borderLeft) { + if (node.position.x < gameServer.border.minx) { gameServer.removeNode(node); i--; - } else if (node.position.x > config.borderRight) { + } else if (node.position.x > gameServer.border.maxx) { gameServer.removeNode(node); i--; - } else if (node.position.y < config.borderTop) { + } else if (node.position.y < gameServer.border.miny) { gameServer.removeNode(node); i--; - } else if (node.position.y > config.borderBottom) { + } else if (node.position.y > gameServer.border.maxy) { gameServer.removeNode(node); i--; } @@ -126,36 +122,34 @@ HungerGames.prototype.onPlayerDeath = function(gameServer) { // Override -HungerGames.prototype.onServerInit = function(gameServer) { +HungerGames.prototype.onServerInit = function (gameServer) { // Prepare this.prepare(gameServer); - + // Resets spawn points this.contenderSpawnPoints = this.baseSpawnPoints.slice(); - + // Override config values if (gameServer.config.serverBots > this.maxContenders) { // The number of bots cannot exceed the maximum amount of contenders gameServer.config.serverBots = this.maxContenders; } gameServer.config.spawnInterval = 20; - gameServer.config.borderLeft = 0; - gameServer.config.borderRight = 6400; - gameServer.config.borderTop = 0; - gameServer.config.borderBottom = 6400; + gameServer.config.borderWidth = 3200; + gameServer.config.borderHeight = 3200; gameServer.config.foodSpawnAmount = 5; // This is hunger games - gameServer.config.foodStartAmount = 100; + gameServer.config.foodMinAmount = 100; gameServer.config.foodMaxAmount = 200; - gameServer.config.foodMass = 2; // Food is scarce, but its worth more + gameServer.config.foodMinSize = 10; // Food is scarce, but its worth more gameServer.config.virusMinAmount = 10; // We need to spawn some viruses in case someone eats them all gameServer.config.virusMaxAmount = 100; gameServer.config.ejectSpawnPlayer = 0; gameServer.config.playerDisconnectTime = 10; // So that people dont disconnect and stall the game for too long - + // Spawn Initial Virus/Large food - var mapWidth = gameServer.config.borderRight - gameServer.config.borderLeft; - var mapHeight = gameServer.config.borderBottom - gameServer.config.borderTop; - + var mapWidth = gameServer.border.width; + var mapHeight = gameServer.border.height; + // Food this.spawnFood(gameServer, 200, { x: mapWidth * .5, @@ -225,7 +219,7 @@ HungerGames.prototype.onServerInit = function(gameServer) { x: mapWidth * .4, y: mapHeight * .7 }); - + // Virus this.spawnVirus(gameServer, { x: mapWidth * .6, @@ -293,13 +287,13 @@ HungerGames.prototype.onServerInit = function(gameServer) { }); }; -HungerGames.prototype.onPlayerSpawn = function(gameServer, player) { +HungerGames.prototype.onPlayerSpawn = function (gameServer, player) { // Only spawn players if the game hasnt started yet if ((this.gamePhase == 0) && (this.contenders.length < this.maxContenders)) { - player.color = gameServer.getRandomColor(); // Random color + player.setColor(gameServer.getRandomColor()); // Random color this.contenders.push(player); // Add to contenders list gameServer.spawnPlayer(player, this.getPos()); - + if (this.contenders.length == this.maxContenders) { // Start the game once there is enough players this.startGamePrep(gameServer); diff --git a/src/gamemodes/Mode.js b/src/gamemodes/Mode.js index 53ecce9a9..0c030e786 100644 --- a/src/gamemodes/Mode.js +++ b/src/gamemodes/Mode.js @@ -1,70 +1,53 @@ -function Mode() { - this.ID = -1; - this.name = "Blank"; - this.decayMod = 1.0; // Modifier for decay rate (Multiplier) - this.packetLB = 49; // Packet id for leaderboard packet (48 = Text List, 49 = List, 50 = Pie chart) - this.haveTeams = false; // True = gamemode uses teams, false = gamemode doesnt use teams - - this.specByLeaderboard = false; // false = spectate from player list instead of leaderboard -} - -module.exports = Mode; - -// Override these - -Mode.prototype.onServerInit = function(gameServer) { - // Called when the server starts - gameServer.run = true; -}; - -Mode.prototype.onTick = function(gameServer) { - // Called on every game tick -}; - -Mode.prototype.onChange = function(gameServer) { - // Called when someone changes the gamemode via console commands -}; - -Mode.prototype.onPlayerInit = function(player) { - // Called after a player object is constructed -}; - -Mode.prototype.onPlayerSpawn = function(gameServer, player) { - // Called when a player is spawned - player.color = gameServer.getRandomColor(); // Random color - gameServer.spawnPlayer(player); -}; - -Mode.prototype.pressQ = function(gameServer, player) { - // Called when the Q key is pressed - if (player.spectate) { - if (!player.freeRoam) player.freeRoam = true; - else player.freeRoam = false; - } -}; - -Mode.prototype.pressW = function(gameServer, player) { - // Called when the W key is pressed - gameServer.ejectMass(player); -}; - -Mode.prototype.pressSpace = function(gameServer, player) { - // Called when the Space bar is pressed - gameServer.splitCells(player); -}; - -Mode.prototype.onCellAdd = function(cell) { - // Called when a player cell is added -}; - -Mode.prototype.onCellRemove = function(cell) { - // Called when a player cell is removed -}; - -Mode.prototype.onCellMove = function(x1, y1, cell) { - // Called when a player cell is moved -}; - -Mode.prototype.updateLB = function(gameServer) { - // Called when the leaderboard update function is called -}; +function Mode() { + this.ID = -1; + this.name = "Blank"; + this.decayMod = 1.0; // Modifier for decay rate (Multiplier) + this.packetLB = 49; // Packet id for leaderboard packet (48 = Text List, 49 = List, 50 = Pie chart) + this.haveTeams = false; // True = gamemode uses teams, false = gamemode doesnt use teams + + this.specByLeaderboard = false; // false = spectate from player list instead of leaderboard +} + +module.exports = Mode; + +// Override these + +Mode.prototype.onServerInit = function (gameServer) { + // Called when the server starts + gameServer.run = true; +}; + +Mode.prototype.onTick = function (gameServer) { + // Called on every game tick +}; + +Mode.prototype.onChange = function (gameServer) { + // Called when someone changes the gamemode via console commands +}; + +Mode.prototype.onPlayerInit = function (player) { + // Called after a player object is constructed +}; + +Mode.prototype.onPlayerSpawn = function (gameServer, player) { + // Called when a player is spawned + player.setColor(gameServer.getRandomColor()); // Random color + gameServer.spawnPlayer(player); +}; + +Mode.prototype.onCellAdd = function (cell) { + // Called when a player cell is added +}; + +Mode.prototype.onCellRemove = function (cell) { + // Called when a player cell is removed +}; + +Mode.prototype.onCellMove = function (cell, gameServer) { + // Called when a player cell is moved +}; + +Mode.prototype.updateLB = function (gameServer) { + gameServer.leaderboardType = this.packetLB; + // Called when the leaderboard update function is called +}; diff --git a/src/gamemodes/Rainbow.js b/src/gamemodes/Rainbow.js index 49b94154f..d39c56e1d 100644 --- a/src/gamemodes/Rainbow.js +++ b/src/gamemodes/Rainbow.js @@ -1,14 +1,13 @@ -var FFA = require('./FFA'); // Base gamemode +var FFA = require('./FFA'); // Base gamemode var Food = require('../entity/Food'); -var FoodUp = require('../entity/Food').prototype.sendUpdate; function Rainbow() { FFA.apply(this, Array.prototype.slice.call(arguments)); - + this.ID = 20; this.name = "Rainbow FFA"; this.specByLeaderboard = true; - + this.colors = [{ 'r': 255, 'g': 0, @@ -139,43 +138,37 @@ Rainbow.prototype = new FFA(); // Gamemode Specific Functions -Rainbow.prototype.changeColor = function(node) { +Rainbow.prototype.changeColor = function (node) { if (typeof node.rainbow == 'undefined') { node.rainbow = Math.floor(Math.random() * this.colors.length); } - + if (node.rainbow >= this.colorsLength) { node.rainbow = 0; } - - node.color = this.colors[node.rainbow]; + + node.setColor(this.colors[node.rainbow]); node.rainbow += this.speed; }; // Override -Rainbow.prototype.onServerInit = function() { - // Overrides the update function - Food.prototype.sendUpdate = function() { - return true; - }; +Rainbow.prototype.onServerInit = function () { }; -Rainbow.prototype.onChange = function() { - // Reset - Food.prototype.sendUpdate = FoodUp; +Rainbow.prototype.onChange = function () { }; -Rainbow.prototype.onTick = function(gameServer) { +Rainbow.prototype.onTick = function (gameServer) { var color, node; // Change color for (var i in gameServer.nodes) { node = gameServer.nodes[i]; - + if (!node) { continue; } - + this.changeColor(node); } }; diff --git a/src/gamemodes/TeamX.js b/src/gamemodes/TeamX.js index 1391fea2e..0455d28bb 100644 --- a/src/gamemodes/TeamX.js +++ b/src/gamemodes/TeamX.js @@ -1,3 +1,4 @@ +// TODO: fix this game mode has outdated code and probably will not works var Teams = require('./Teams.js'); var Cell = require('../entity/Cell.js'); var Food = require('../entity/Food.js'); @@ -10,10 +11,10 @@ var GS_getCellsInRange = null; function TeamX() { Teams.apply(this, Array.prototype.slice.call(arguments)); - + this.ID = 14; this.name = 'Experimental Team'; - + // configurations: this.teamCollision = false; // set to true to disable eating teammates this.pushVirus = false; // true: pushing virus, false: splitting virus @@ -22,21 +23,21 @@ function TeamX() { this.motherUpdateInterval = 5; // How many ticks it takes to update the mother cell (1 tick = 50 ms) this.motherSpawnInterval = 100; // How many ticks it takes to spawn another mother cell - Currently 5 seconds this.motherMinAmount = 5; - + // game mode data: this.colors = [{ - 'r': 255, - 'g': 7, - 'b': 7 - }, { - 'r': 7, - 'g': 255, - 'b': 7 - }, { - 'r': 7, - 'g': 7, - 'b': 255 - }, ]; + 'r': 255, + 'g': 7, + 'b': 7 + }, { + 'r': 7, + 'g': 255, + 'b': 7 + }, { + 'r': 7, + 'g': 7, + 'b': 255 + },]; this.nodesMother = []; this.tickMother = 0; this.tickMotherS = 0; @@ -47,66 +48,44 @@ TeamX.prototype = new Teams(); // Gamemode Specific Functions -TeamX.prototype.updateMotherCells = function(gameServer) { +TeamX.prototype.updateMotherCells = function (gameServer) { for (var i in this.nodesMother) { var mother = this.nodesMother[i]; - + // Checks mother.update(gameServer); mother.checkEat(gameServer); } }; -TeamX.prototype.spawnMotherCell = function(gameServer) { +TeamX.prototype.spawnMotherCell = function (gameServer) { // Checks if there are enough mother cells on the map if (this.nodesMother.length < this.motherMinAmount) { // Spawns a mother cell var pos = gameServer.getRandomPosition(); - + // Check for players - for (var i = 0; i < gameServer.nodesPlayer.length; i++) { - var check = gameServer.nodesPlayer[i]; - - var r = check.getSize(); // Radius of checking player cell - - // Collision box - var topY = check.position.y - r; - var bottomY = check.position.y + r; - var leftX = check.position.x - r; - var rightX = check.position.x + r; - - // Check for collisions - if (pos.y > bottomY) { - continue; - } - - if (pos.y < topY) { - continue; - } - - if (pos.x > rightX) { - continue; - } - - if (pos.x < leftX) { - continue; - } - - // Collided + var size = Math.sqrt(this.motherCellMass * 100); + var bound = { + minx: pos.x - size, + miny: pos.y - size, + maxx: pos.x + size, + maxy: pos.y + size + }; + if (gameServer.quadTree.any(bound, function (item) { return item.cell.cellType == 0; })) { return; } - // Spawn if no cells are colliding var m = new MotherCell(gameServer.getNextNodeId(), null, pos, this.motherCellMass); gameServer.addNode(m); } }; -TeamX.prototype.countNotInRange = function(client) { +TeamX.prototype.countNotInRange = function (client) { var count = 0; for (var i = 0; i < client.cells.length; i++) { var cell = client.cells[i]; - if (!(cell.inRange === true)) { + if (!(cell.isRemoved === true)) { count++; } } @@ -115,14 +94,14 @@ TeamX.prototype.countNotInRange = function(client) { // Overwrite: -TeamX.prototype.fuzzColorComponent = function(component) { +TeamX.prototype.fuzzColorComponent = function (component) { if (component != 255) { component = Math.random() * (this.colorFuzziness - 7) + 7; } return component; }; -TeamX.prototype.getTeamColor = function(team) { +TeamX.prototype.getTeamColor = function (team) { var color = this.colors[team]; return { r: this.fuzzColorComponent(color.r), @@ -131,138 +110,31 @@ TeamX.prototype.getTeamColor = function(team) { }; }; -TeamX.prototype.onServerInit = function(gameServer) { +TeamX.prototype.onServerInit = function (gameServer) { // Set up teams for (var i = 0; i < this.teamAmount; i++) { this.nodes[i] = []; } - + // Special virus mechanics if (this.pushVirus) { - Virus.prototype.feed = function(feeder, gameServer) { - gameServer.removeNode(feeder); + Virus.prototype.onEat = function (prey) { // Pushes the virus - this.setAngle(feeder.getAngle()); // Set direction if the virus explodes - this.moveEngineTicks = 5; // Amount of times to loop the movement function - this.moveEngineSpeed = 30; - - var index = gameServer.movingNodes.indexOf(this); - if (index == -1) { - gameServer.movingNodes.push(this); - } + var angle = prey.isMoving ? prey.getAngle() : this.getAngle(); + this.setBoost(16 * 20, angle); }; } - - if (!this.teamCollision) { - this.onCellMove = function(x1, y1, cell) {}; // does nothing - if (GS_getCellsInRange == null) - GS_getCellsInRange = gameServer.getCellsInRange; - - gameServer.getCellsInRange = function(cell) { - var list = new Array(); - var squareR = cell.getSquareSize(); // Get cell squared radius - - // Loop through all cells that are visible to the cell. There is probably a more efficient way of doing this but whatever - var len = cell.owner.visibleNodes.length; - for (var i = 0; i < len; i++) { - var check = cell.owner.visibleNodes[i]; - - if (typeof check === 'undefined') { - continue; - } - - // if something already collided with this cell, don't check for other collisions - if (check.inRange) { - continue; - } - - // Can't eat itself - if (cell.nodeId == check.nodeId) { - continue; - } - - // Can't eat cells that have collision turned off - if ((cell.owner == check.owner) && (cell.ignoreCollision)) { - continue; - } - - // AABB Collision - if (!check.collisionCheck2(squareR, cell.position)) { - continue; - } - - // Cell type check - Cell must be bigger than this number times the mass of the cell being eaten - var multiplier = 1.25; - - switch (check.getType()) { - case 1: // Food cell - list.push(check); - check.inRange = true; // skip future collision checks for this food - continue; - case 2: // Virus - multiplier = 1.33; - break; - case 0: // Players - // Can't eat self if it's not time to recombine yet - if (check.owner == cell.owner) { - if ((cell.recombineTicks > 0) || (check.recombineTicks > 0)) { - continue; - } - - multiplier = 1.00; - } - - // Can't eat team members - if (this.gameMode.haveTeams) { - if (!check.owner) { // Error check - continue; - } - - if ((check.owner != cell.owner) && (check.owner.getTeam() == cell.owner.getTeam()) && this.gameMode.countNotInRange(check.owner) == 1) { - continue; - } - } - break; - default: - break; - } - - // Make sure the cell is big enough to be eaten. - if ((check.mass * multiplier) > cell.mass) { - continue; - } - - // Eating range - var xs = Math.pow(check.position.x - cell.position.x, 2); - var ys = Math.pow(check.position.y - cell.position.y, 2); - var dist = Math.sqrt(xs + ys); - - var eatingRange = cell.getSize() - check.getEatingRange(); // Eating range = radius of eating cell + 40% of the radius of the cell being eaten - if (dist > eatingRange) { - // Not in eating range - continue; - } - - // Add to list of cells nearby - list.push(check); - - // Something is about to eat this cell; no need to check for other collisions with it - check.inRange = true; - } - return list; - }; - } - + if (GS_getRandomColor == null) GS_getRandomColor = gameServer.getRandomColor; // backup if (GS_getRandomSpawn == null) GS_getRandomSpawn = gameServer.getRandomSpawn; - + // Override this gameServer.getRandomSpawn = gameServer.getRandomPosition; - gameServer.getRandomColor = function() { + gameServer.getRandomColor = function () { var colorRGB = [0xFF, 0x07, (Math.random() * 256) >> 0]; - colorRGB.sort(function() { + colorRGB.sort(function () { return 0.5 - Math.random(); }); return { @@ -271,21 +143,21 @@ TeamX.prototype.onServerInit = function(gameServer) { g: colorRGB[2] }; }; - + // migrate current players to team mode for (var i = 0; i < gameServer.clients.length; i++) { var client = gameServer.clients[i].playerTracker; this.onPlayerInit(client); - client.color = this.getTeamColor(client.team); + client.setColor(this.getTeamColor(client.team)); for (var j = 0; j < client.cells.length; j++) { var cell = client.cells[j]; - cell.setColor(client.color); + cell.setColor(client.getColor()); this.nodes[client.team].push(cell); } } }; -TeamX.prototype.onChange = function(gameServer) { +TeamX.prototype.onChange = function (gameServer) { // Remove all mother cells for (var i in this.nodesMother) { gameServer.removeNode(this.nodesMother[i]); @@ -303,7 +175,7 @@ TeamX.prototype.onChange = function(gameServer) { } }; -TeamX.prototype.onTick = function(gameServer) { +TeamX.prototype.onTick = function (gameServer) { // Mother Cell updates if (this.tickMother >= this.motherUpdateInterval) { this.updateMotherCells(gameServer); @@ -311,7 +183,7 @@ TeamX.prototype.onTick = function(gameServer) { } else { this.tickMother++; } - + // Mother Cell Spawning if (this.tickMotherS >= this.motherSpawnInterval) { this.spawnMotherCell(gameServer); @@ -326,86 +198,34 @@ TeamX.prototype.onTick = function(gameServer) { function MotherCell() { // Temporary - Will be in its own file if Zeach decides to add this to vanilla Cell.apply(this, Array.prototype.slice.call(arguments)); - + this.cellType = 2; // Copies virus cell - this.color = { - r: 205, - g: 85, - b: 100 - }; - this.spiked = 1; + this.setColor({ r: 205, g: 85, b: 100 }); + this.isSpiked = true; } MotherCell.prototype = new Cell(); // Base -MotherCell.prototype.getEatingRange = function() { - return this.getSize() * .5; -}; - -MotherCell.prototype.update = function(gameServer) { +MotherCell.prototype.update = function (gameServer) { // Add mass - this.mass += .25; - + this.setSize(Math.sqrt(this.getSize() * this.getSize() + 0.25 * 0.25)); + // Spawn food var maxFood = 10; // Max food spawned per tick var i = 0; // Food spawn counter - while ((this.mass > gameServer.gameMode.motherCellMass) && (i < maxFood)) { + while ((this.getMass() > gameServer.gameMode.motherCellMass) && (i < maxFood)) { // Only spawn if food cap hasn been reached if (gameServer.currentFood < gameServer.config.foodMaxAmount) { this.spawnFood(gameServer); } - + // Incrementers - this.mass--; + this.setSize(Math.sqrt(this.getSize() * this.getSize() - 1)); i++; } }; -MotherCell.prototype.checkEat = function(gameServer) { - var safeMass = this.mass * .9; - var r = this.getSize(); // The box area that the checked cell needs to be in to be considered eaten - - // Loop for potential prey - for (var i in gameServer.nodesPlayer) { - var check = gameServer.nodesPlayer[i]; - - if (check.mass > safeMass) { - // Too big to be consumed - continue; - } - - // Calculations - var len = r - (check.getSize() / 2) >> 0; - if ((this.abs(this.position.x - check.position.x) < len) && (this.abs(this.position.y - check.position.y) < len)) { - // Eats the cell - gameServer.removeNode(check); - this.mass += check.mass; - } - } - for (var i in gameServer.movingNodes) { - var check = gameServer.movingNodes[i]; - - if ((check.getType() == 1) || (check.mass > safeMass)) { - // Too big to be consumed/ No player cells - continue; - } - - // Calculations - var len = r >> 0; - if ((this.abs(this.position.x - check.position.x) < len) && (this.abs(this.position.y - check.position.y) < len)) { - // Eat the cell - gameServer.removeNode(check); - this.mass += check.mass; - } - } -}; - -MotherCell.prototype.abs = function(n) { - // Because Math.abs is slow - return (n < 0) ? -n : n; -}; - -MotherCell.prototype.spawnFood = function(gameServer) { +MotherCell.prototype.spawnFood = function (gameServer) { // Get starting position var angle = Math.random() * 6.28; // (Math.PI * 2) ??? Precision is not our greatest concern here var r = this.getSize(); @@ -413,29 +233,26 @@ MotherCell.prototype.spawnFood = function(gameServer) { x: this.position.x + (r * Math.sin(angle)), y: this.position.y + (r * Math.cos(angle)) }; - + // Spawn food - var f = new Food(gameServer.getNextNodeId(), null, pos, gameServer.config.foodMass, gameServer); - f.setColor(gameServer.getRandomColor()); - - gameServer.addNode(f); - gameServer.currentFood++; - + var cell = new Food(gameServer.getNextNodeId(), null, pos, gameServer.config.foodMinSize, gameServer); + cell.setColor(gameServer.getRandomColor()); + + gameServer.addNode(cell); + // Move engine - f.angle = angle; var dist = (Math.random() * 10) + 22; // Random distance - f.setMoveEngineData(dist, 15); - - gameServer.setAsMovingNode(f); + // TODO: check distance + cell.setBoost(dist * 15, angle); }; -MotherCell.prototype.onConsume = Virus.prototype.onConsume; // Copies the virus prototype function +MotherCell.prototype.onEaten = Virus.prototype.onEaten; // Copies the virus prototype function -MotherCell.prototype.onAdd = function(gameServer) { +MotherCell.prototype.onAdd = function (gameServer) { gameServer.gameMode.nodesMother.push(this); // Temporary }; -MotherCell.prototype.onRemove = function(gameServer) { +MotherCell.prototype.onRemove = function (gameServer) { var index = gameServer.gameMode.nodesMother.indexOf(this); if (index != -1) { gameServer.gameMode.nodesMother.splice(index, 1); diff --git a/src/gamemodes/TeamZ.js b/src/gamemodes/TeamZ.js index 70ea79cf8..7da98f48a 100644 --- a/src/gamemodes/TeamZ.js +++ b/src/gamemodes/TeamZ.js @@ -1,1172 +1,1132 @@ -var Mode = require('./Mode.js'); -var Cell = require('../entity/Cell.js'); -var Entity = require('../entity'); -var Virus = require('../entity/Virus.js'); - -var GameServer = null; // represent GameServer Type -var GS_getRandomColor = null; // backup getRandomColor function of GameServer type -var GS_getNearestVirus = null; -var GS_getCellsInRange = null; -var GS_splitCells = null; -var GS_newCellVirused = null; -var Virus_onConsume = Virus.prototype.onConsume; - -var GameState = { - WF_PLAYERS: 0, - WF_START: 1, - IN_PROGRESS: 2 -}; - -// new Cell Type IDs of HERO and BRAIN are calculated based on Game Mode ID -var CellType = { - PLAYER: 0, - FOOD: 1, - VIRUS: 2, - EJECTED_MASS: 3, - HERO: 130, - BRAIN: 131 -}; - -var localLB = []; - -function TeamZ() { - Mode.apply(this, Array.prototype.slice.call(arguments)); - - this.ID = 13; - this.name = 'Zombie Team'; - this.packetLB = 48; - this.haveTeams = true; - - // configurations: - this.minPlayer = 2; // game is auto started if there are at least 2 players - this.gameDuration = 18000; // ticks, 1 tick = 50 ms (20 ticks = 1 s) - this.warmUpDuration = 600; // ticks, time to wait between games - this.crazyDuration = 200; // ticks - this.heroEffectDuration = 1000; // ticks - this.brainEffectDuration = 200; // ticks - this.spawnBrainInterval = 1200; // ticks - this.spawnHeroInterval = 600; // ticks - this.defaultColor = { - r: 0x9b, - g: 0x30, - b: 0xff - }; - - this.colorFactorStep = 5; - this.colorLower = 50; // Min 0 - this.colorUpper = 225; // Max 255 - this.maxBrain = -1; // set this param to any negative number to keep the number of brains not exceed number of humans - this.maxHero = 4; // set this param to any negative number to keep the number of heroes not exceed number of zombies - - // game mode data: - this.state = GameState.WF_PLAYERS; - this.winTeam = -1; - this.gameTimer = 0; - this.zombies = []; // the clients of zombie players - this.humans = []; // the clients of human players - - this.heroes = []; - this.brains = []; - - this.spawnHeroTimer = 0; - this.spawnBrainTimer = 0; -} - -module.exports = TeamZ; -TeamZ.prototype = new Mode(); - -// Gamemode Specific Functions - -TeamZ.prototype.createZColorFactor = function(client) { - client.zColorFactor = (Math.random() * (this.colorUpper - this.colorLower + 1)) >> 0 + this.colorLower; - client.zColorIncr = true; // color will be increased if TRUE - otherwise it will be decreased. -}; - -TeamZ.prototype.nextZColorFactor = function(client) { - if (client.zColorIncr == true) { - if (client.zColorFactor + this.colorFactorStep >= this.colorUpper) { - client.zColorFactor = this.colorUpper; - client.zColorIncr = false; - } else { - client.zColorFactor += this.colorFactorStep; - } - } else { - if (client.zColorFactor - this.colorFactorStep <= this.colorLower) { - client.zColorFactor = this.colorLower; - client.zColorIncr = true; - } else { - client.zColorFactor -= this.colorFactorStep; - } - } -}; - -TeamZ.prototype.updateZColor = function(client, mask) { - var color = { - r: (mask & 0x4) > 0 ? client.zColorFactor : 7, - g: (mask & 0x2) > 0 ? client.zColorFactor : 7, - b: (mask & 0x1) > 0 ? client.zColorFactor : 7 - }; - client.color = { - r: color.r, - g: color.g, - b: color.b - }; - for (var i = 0; i < client.cells.length; i++) { - var cell = client.cells[i]; - cell.setColor(color); - } -}; - -TeamZ.prototype.isCrazy = function(client) { - return (typeof(client.crazyTimer) != 'undefined' && client.crazyTimer > 0 && client.team > 0); -}; - -TeamZ.prototype.hasEatenHero = function(client) { - return (typeof(client.eatenHeroTimer) != 'undefined' && client.eatenHeroTimer > 0); -}; - -TeamZ.prototype.hasEatenBrain = function(client) { - return (typeof(client.eatenBrainTimer) != 'undefined' && client.eatenBrainTimer > 0); -}; - -TeamZ.prototype.spawnDrug = function(gameServer, cell) { // spawn HERO or BRAIN - var max = 0; - var proceedNext = false; - if (cell.getType() == CellType.HERO) { - max = this.maxHero < 0 ? this.zombies.length : this.maxHero; - proceedNext = this.heroes.length < max; - } else if (cell.getType() == CellType.BRAIN) { - max = this.maxBrain < 0 ? this.humans.length : this.maxBrain; - proceedNext = this.brains.length < max; - } - if (proceedNext) { - var pos = gameServer.getRandomPosition(); - - // Check for players - var collided = false; - for (var i = 0; i < gameServer.nodesPlayer.length; i++) { - var check = gameServer.nodesPlayer[i]; - var r = check.getSize(); // Radius of checking player cell - - // Collision box - var topY = check.position.y - r; - var bottomY = check.position.y + r; - var leftX = check.position.x - r; - var rightX = check.position.x + r; - - // Check for collisions - if (pos.y > bottomY) { - continue; - } - if (pos.y < topY) { - continue; - } - if (pos.x > rightX) { - continue; - } - if (pos.x < leftX) { - continue; - } - - // Collided - collided = true; - break; - } - - // Spawn if no cells are colliding - if (!collided) { - cell.position = pos; - gameServer.addNode(cell); - return true; // SUCCESS with spawn - } - return false; // FAILED because of collision - } - return true; // SUCCESS without spawn -}; - -// Call to change a human client to a zombie -TeamZ.prototype.turnToZombie = function(client) { - client.team = 0; // team Z - this.createZColorFactor(client); - this.updateZColor(client, 0x7); // Gray - - // remove from human list - var index = this.humans.indexOf(client); - if (index >= 0) { - this.humans.splice(index, 1); - } - - // add to zombie list - this.zombies.push(client); -}; - -TeamZ.prototype.boostSpeedCell = function(cell) { - if (typeof cell.originalSpeed == 'undefined' || cell.originalSpeed == null) { - cell.originalSpeed = cell.getSpeed; - cell.getSpeed = function() { - return 2 * this.originalSpeed(); - }; - } -}; - -TeamZ.prototype.boostSpeed = function(client) { - for (var i = 0; i < client.cells.length; i++) { - var cell = client.cells[i]; - if (typeof cell == 'undefined') - continue; - this.boostSpeedCell(cell); - } -}; - -TeamZ.prototype.resetSpeedCell = function(cell) { - if (typeof cell.originalSpeed != 'undefined' && cell.originalSpeed != null) { - cell.getSpeed = cell.originalSpeed; - cell.originalSpeed = null; - } -}; - -TeamZ.prototype.resetSpeed = function(client) { - for (var i = 0; i < client.cells.length; i++) { - var cell = client.cells[i]; - if (typeof cell == 'undefined') - continue; - this.resetSpeedCell(cell); - } -}; - -TeamZ.prototype.startGame = function(gameServer) { - for (var i = 0; i < this.humans.length; i++) { - var client = this.humans[i]; - client.team = client.pID; - client.crazyTimer = 0; - client.eatenHeroTimer = 0; - client.eatenBrainTimer = 0; - client.color = gameServer.getRandomColor(); - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - if (cell) { - cell.setColor(client.color); - cell.mass = gameServer.config.playerStartMass; - this.resetSpeedCell(cell); - } - } - } - - // Select random human to be the zombie - var zombie = this.humans[(Math.random() * this.humans.length) >> 0]; - this.turnToZombie(zombie); - - this.winTeam = -1; - this.state = GameState.IN_PROGRESS; - this.gameTimer = this.gameDuration; -}; - -TeamZ.prototype.endGame = function(gameServer) { - // reset game - for (var i = 0; i < this.zombies.length; i++) { - var client = this.zombies[i]; - var index = this.humans.indexOf(client); - if (index < 0) { - this.humans.push(client); - } - } - this.zombies = []; - this.spawnHeroTimer = 0; - this.spawnBrainTimer = 0; - localLB = []; // reset leader board - - for (var i = 0; i < this.humans.length; i++) { - var client = this.humans[i]; - client.color = this.defaultColor; - client.team = 1; - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - cell.setColor(this.defaultColor); - } - } - - this.state = GameState.WF_PLAYERS; - this.gameTimer = 0; -}; - -TeamZ.prototype.leaderboardAddSort = function(player, leaderboard) { - // Adds the player and sorts the leaderboard - var len = leaderboard.length - 1; - var loop = true; - - while ((len >= 0) && (loop)) { - // Start from the bottom of the leaderboard - if (player.getScore(false) <= leaderboard[len].getScore(false)) { - leaderboard.splice(len + 1, 0, player); - loop = false; // End the loop if a spot is found - } - len--; - } - if (loop) { - // Add to top of the list because no spots were found - leaderboard.splice(0, 0, player); - } -}; - -// Override - -TeamZ.prototype.onServerInit = function(gameServer) { - // Called when the server starts - gameServer.run = true; - - // Overwrite some server functions: - GameServer = require('../GameServer.js'); - GS_getRandomColor = GameServer.prototype.getRandomColor; // backup - GS_getNearestVirus = GameServer.prototype.getNearestVirus; - GS_getCellsInRange = GameServer.prototype.getCellsInRange; - GS_splitCells = GameServer.prototype.splitCells; - GS_newCellVirused = GameServer.prototype.newCellVirused; - - //OVERWRITE GLOBAL FUNCTIONs to adapt Zombie Team mode - - // Change to AGARIO colorful scheme - GameServer.prototype.getRandomColor = function() { - var colorRGB = [0xFF, 0x07, (Math.random() * 256) >> 0]; - colorRGB.sort(function() { - return 0.5 - Math.random(); - }); - return { - r: colorRGB[0], - b: colorRGB[1], - g: colorRGB[2] - }; - }; - - GameServer.prototype.getNearestVirus = function(cell) { - // More like getNearbyVirus - var virus = null; - var r = 100; // Checking radius - - var topY = cell.position.y - r; - var bottomY = cell.position.y + r; - - var leftX = cell.position.x - r; - var rightX = cell.position.x + r; - - // loop through all heroes - for (var i = 0; i < this.gameMode.heroes.length; i++) { - var check = this.gameMode.heroes[i]; - if (typeof check === 'undefined') { - continue; - } - if (!check.collisionCheck(bottomY, topY, rightX, leftX)) { - continue; - } - virus = check; - break; - } - if (virus != null) - return virus; - - // loop through all brains - for (var i = 0; i < this.gameMode.brains.length; i++) { - var check = this.gameMode.brains[i]; - if (typeof check === 'undefined') { - continue; - } - if (!check.collisionCheck(bottomY, topY, rightX, leftX)) { - continue; - } - virus = check; - break; - } - - if (virus != null) - return virus; - - // Call base: - // Loop through all viruses on the map. There is probably a more efficient way of doing this but whatever - var len = this.nodesVirus.length; - for (var i = 0; i < len; i++) { - var check = this.nodesVirus[i]; - - if (typeof check === 'undefined') { - continue; - } - - if (!check.collisionCheck(bottomY, topY, rightX, leftX)) { - continue; - } - - // Add to list of cells nearby - virus = check; - break; // stop checking when a virus found - } - return virus; - }; - - // this is almost same to the legacy function - GameServer.prototype.getCellsInRange = function(cell) { - var list = new Array(); - - if (this.gameMode.state != GameState.IN_PROGRESS) - return list; - - var squareR = cell.getSquareSize(); // Get cell squared radius - - // Loop through all cells that are visible to the cell. There is probably a more efficient way of doing this but whatever - var len = cell.owner.visibleNodes.length; - for (var i = 0; i < len; i++) { - var check = cell.owner.visibleNodes[i]; - - if (typeof check === 'undefined') { - continue; - } - - // if something already collided with this cell, don't check for other collisions - if (check.inRange) { - continue; - } - - // HERO and BRAIN checking - if (cell.owner.getTeam() == 0) { - // Z team - if (check.getType() == CellType.HERO) - continue; - } else { - // H team - if (check.getType() == CellType.BRAIN) - continue; - } - - // Can't eat itself - if (cell.nodeId == check.nodeId) { - continue; - } - - // Can't eat cells that have collision turned off - if ((cell.owner == check.owner) && (cell.ignoreCollision)) { - continue; - } - - // AABB Collision - if (!check.collisionCheck2(squareR, cell.position)) { - continue; - } - - // Cell type check - Cell must be bigger than this number times the mass of the cell being eaten - var multiplier = 1.25; - - switch (check.getType()) { - case 1: // Food cell - list.push(check); - check.inRange = true; // skip future collision checks for this food - continue; - case 2: // Virus - multiplier = 1.33; - break; - case 0: // Players - // Can't eat self if it's not time to recombine yet - if (check.owner == cell.owner) { - if ((cell.recombineTicks > 0) || (check.recombineTicks > 0)) { - continue; - } - - multiplier = 1.00; - } - - // Can't eat team members - if (this.gameMode.haveTeams) { - if (!check.owner) { // Error check - continue; - } - - if ((check.owner != cell.owner) && (check.owner.getTeam() == cell.owner.getTeam())) { - continue; - } - } - break; - default: - break; - } - - // Make sure the cell is big enough to be eaten. - if ((check.mass * multiplier) > cell.mass) { - continue; - } - - // Eating range - var xs = Math.pow(check.position.x - cell.position.x, 2); - var ys = Math.pow(check.position.y - cell.position.y, 2); - var dist = Math.sqrt(xs + ys); - - var eatingRange = cell.getSize() - check.getEatingRange(); // Eating range = radius of eating cell + 40% of the radius of the cell being eaten - if (dist > eatingRange) { - // Not in eating range - continue; - } - - // Add to list of cells nearby - list.push(check); - - // Something is about to eat this cell; no need to check for other collisions with it - check.inRange = true; - } - return list; - }; - - // this is almost same to the legacy function - GameServer.prototype.splitCells = function(client) { - var len = client.cells.length; - for (var i = 0; i < len; i++) { - if (client.cells.length >= this.config.playerMaxCells) { - // Player cell limit - continue; - } - - var cell = client.cells[i]; - if (!cell) { - continue; - } - - if (cell.mass < this.config.playerMinMassSplit) { - continue; - } - - // Get angle - var deltaY = client.mouse.y - cell.position.y; - var deltaX = client.mouse.x - cell.position.x; - var angle = Math.atan2(deltaX, deltaY); - - // Get starting position - var size = cell.getSize() / 2; - var startPos = { - x: cell.position.x + (size * Math.sin(angle)), - y: cell.position.y + (size * Math.cos(angle)) - }; - // Calculate mass and speed of splitting cell - var splitSpeed = cell.getSpeed() * 6; - var newMass = cell.mass / 2; - cell.mass = newMass; - // Create cell - var split = new Entity.PlayerCell(this.getNextNodeId(), client, startPos, newMass); - split.setAngle(angle); - split.setMoveEngineData(splitSpeed, 32, 0.85); - split.calcMergeTime(this.config.playerRecombineTime); - - // boost speed if zombie eats brain - if (this.gameMode.hasEatenBrain(client) || this.gameMode.isCrazy(client)) { - this.gameMode.boostSpeedCell(split); - } - // gain effect if human eat hero - else if (this.gameMode.hasEatenHero(client)) { - // fix "unable to split" bug: cell can be merged after finish moving (2nd param in setMoveEngineData) - split.recombineTicks = 2; // main-ticks, 1 main-tick = 1 s - } - - // Add to moving cells list - this.setAsMovingNode(split); - this.addNode(split); - } - }; - - // this function is almost same to the legacy - GameServer.prototype.newCellVirused = function(client, parent, angle, mass, speed) { - // Starting position - var startPos = { - x: parent.position.x, - y: parent.position.y - }; - - // Create cell - newCell = new Entity.PlayerCell(this.getNextNodeId(), client, startPos, mass); - newCell.setAngle(angle); - newCell.setMoveEngineData(speed, 10); - newCell.calcMergeTime(this.config.playerRecombineTime); - newCell.ignoreCollision = true; // Turn off collision - - // boost speed if zombie eats brain - if (this.gameMode.hasEatenBrain(client) || this.gameMode.isCrazy(client)) { - this.gameMode.boostSpeedCell(newCell); - } - // gain effect if human eat hero - else if (this.gameMode.hasEatenHero(client)) { - // fix "unable to split" bug - newCell.recombineTicks = 1; - } - - // Add to moving cells list - this.addNode(newCell); - this.setAsMovingNode(newCell); - }; - - Virus.prototype.onConsume = function(consumer, gameServer) { - var client = consumer.owner; - - var maxSplits = Math.floor(consumer.mass / 16) - 1; // Maximum amount of splits - var numSplits = gameServer.config.playerMaxCells - client.cells.length; // Get number of splits - numSplits = Math.min(numSplits, maxSplits); - var splitMass = Math.min(consumer.mass / (numSplits + 1), 36); // Maximum size of new splits - - // Cell consumes mass before splitting - consumer.addMass(this.mass); - - // Cell cannot split any further - if (numSplits <= 0) { - return; - } - - // Big cells will split into cells larger than 36 mass (1/4 of their mass) - var bigSplits = 0; - var endMass = consumer.mass - (numSplits * splitMass); - if ((endMass > 300) && (numSplits > 0)) { - bigSplits++; - numSplits--; - } - if ((endMass > 1200) && (numSplits > 0)) { - bigSplits++; - numSplits--; - } - if ((endMass > 3000) && (numSplits > 0)) { - bigSplits++; - numSplits--; - } - - // Splitting - var angle = 0; // Starting angle - for (var k = 0; k < numSplits; k++) { - angle += 6 / numSplits; // Get directions of splitting cells - gameServer.newCellVirused(client, consumer, angle, splitMass, 150); - consumer.mass -= splitMass; - } - - for (var k = 0; k < bigSplits; k++) { - angle = Math.random() * 6.28; // Random directions - splitMass = consumer.mass / 4; - gameServer.newCellVirused(client, consumer, angle, splitMass, 20); - consumer.mass -= splitMass; - } - - if (gameServer.gameMode.hasEatenHero(client)) - consumer.recombineTicks = 0; - else - consumer.calcMergeTime(gameServer.config.playerRecombineTime); - }; - - // Handle "gamemode" command: - for (var i = 0; i < gameServer.clients.length; i++) { - var client = gameServer.clients[i].playerTracker; - if (!client) - continue; - - if (client.cells.length > 0) { - client.eatenBrainTimer = 0; - client.eatenHeroTimer = 0; - client.crazyTimer = 0; - client.color = this.defaultColor; - client.team = 1; - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - cell.setColor(this.defaultColor); - } - this.humans.push(client); - } - } -}; - -TeamZ.prototype.onChange = function(gameServer) { - // Called when someone changes the gamemode via console commands - // remove Brain and Hero - for (var i = 0; this.brains.length; i++) { - var node = this.brains[i]; - gameServer.removeNode(node); - } - for (var i = 0; this.heroes.length; i++) { - var node = this.heroes[i]; - gameServer.removeNode(node); - } - - // discard all boost: - for (var i = 0; i < this.humans.length; i++) { - var client = this.humans[i]; - if (this.isCrazy(client)) { - this.resetSpeed(client); - } - } - for (var i = 0; i < this.zombies.length; i++) { - var client = this.zombies[i]; - if (this.hasEatenBrain(client)) { - this.resetSpeed(client); - } - } - - // revert to default: - GameServer.prototype.getRandomColor = GS_getRandomColor; - GameServer.prototype.getNearestVirus = GS_getNearestVirus; - GameServer.prototype.getCellsInRange = GS_getCellsInRange; - GameServer.prototype.splitCells = GS_splitCells; - GameServer.prototype.newCellVirused = GS_newCellVirused; - Virus.prototype.onConsume = Virus_onConsume; -}; - -TeamZ.prototype.onTick = function(gameServer) { - // Called on every game tick - - switch (this.state) { - case GameState.WF_PLAYERS: - if (this.humans.length >= this.minPlayer) { - this.state = GameState.WF_START; - this.gameTimer = this.warmUpDuration; - } - break; - case GameState.WF_START: - this.gameTimer--; - if (this.gameTimer == 0) { - if (this.humans.length >= this.minPlayer) { - // proceed: - this.startGame(gameServer); - } else { - // back to previous state: - this.state = GameState.WF_PLAYERS; - } - } - break; - case GameState.IN_PROGRESS: - this.gameTimer--; - if (this.gameTimer == 0) { - // human wins - this.winTeam = 1; - } else { - if (this.humans.length == 0) { // no human left - // zombie wins - this.winTeam = 0; - } else if (this.zombies.length == 0) { // no zombie left - // human wins - this.winTeam = 1; - } - } - - if (this.winTeam >= 0) { - this.endGame(gameServer); - } - - break; - } - - // change color of zombies - for (var i = 0; i < this.zombies.length; i++) { - var client = this.zombies[i]; - this.nextZColorFactor(client); - - if (this.hasEatenBrain(client)) { - client.eatenBrainTimer--; - - if (client.eatenBrainTimer > 0) { - this.updateZColor(client, 0x5); // Pink - continue; - } else { - // reset speed: - this.resetSpeed(client); - } - } - - this.updateZColor(client, 0x7); // Gray - } - - for (var i = 0; i < this.humans.length; i++) { - var client = this.humans[i]; - if (this.isCrazy(client)) { - client.crazyTimer--; - if (client.crazyTimer == 0) { - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - // reset speed: - this.resetSpeedCell(cell); - - // reset color: - if (client.cured == true) - cell.setColor(client.color); - } - - if (client.cured == true) { - client.cured = false; // reset - } else { - // turn player to zombie - this.turnToZombie(client); - continue; - } - } else { - client.colorToggle++; - if (client.colorToggle % 10 == 0) { - var blinkColor = null; - - if (client.colorToggle == 20) { - blinkColor = client.color; - client.colorToggle = 0; - } else { - if (client.cured == true) { - blinkColor = { - r: 255, - g: 255, - b: 7 - }; // Yellow - } else { - blinkColor = { - r: 75, - g: 75, - b: 75 - }; // Gray - } - } - - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - cell.setColor(blinkColor); - } - } - } - } else if (this.hasEatenHero(client)) { - client.eatenHeroTimer--; - var color = null; - if (client.eatenHeroTimer > 0) { - client.heroColorFactor = (client.heroColorFactor + 5) % 401; - if (client.heroColorFactor <= 200) { - color = { - r: 255, - g: 255, - b: client.heroColorFactor - }; // Yellow scheme - } else { - color = { - r: 255, - g: 255, - b: 400 - client.heroColorFactor - }; // Yellow scheme - } - } else { - color = client.color; // reset - } - - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - cell.setColor(color); - } - } - } - - // check timer to spawn Hero: - this.spawnHeroTimer++; - if (this.spawnHeroTimer >= this.spawnHeroInterval) { - this.spawnHeroTimer = 0; - var cell = new Hero(gameServer.getNextNodeId(), null); - while (!this.spawnDrug(gameServer, cell)); // collision detect algorithm needs enhancement - } - - // check timer to spawn Brain: - this.spawnBrainTimer++; - if (this.spawnBrainTimer >= this.spawnBrainInterval) { - this.spawnBrainTimer = 0; - var cell = new Brain(gameServer.getNextNodeId(), null); - while (!this.spawnDrug(gameServer, cell)); // collision detect algorithm needs enhancement - } -}; - -TeamZ.prototype.onCellAdd = function(cell) { - // Called when a player cell is added - var client = cell.owner; - if (client.cells.length == 1) { // first cell - client.team = client.pID; - client.color = { - r: cell.color.r, - g: cell.color.g, - b: cell.color.b - }; - client.eatenBrainTimer = 0; - client.eatenHeroTimer = 0; - client.crazyTimer = 0; - this.humans.push(client); - - if (this.state == GameState.IN_PROGRESS) { - this.turnToZombie(client); - } else { - client.color = this.defaultColor; - cell.setColor(this.defaultColor); - client.team = 1; // game not started yet - } - } -}; - -TeamZ.prototype.onCellRemove = function(cell) { - // Called when a player cell is removed - var client = cell.owner; - if (client.cells.length == 0) { // last cell - if (client.getTeam() == 0) { - // Z team - var index = this.zombies.indexOf(client); - if (index >= 0) - this.zombies.splice(index, 1); - } else { - // H team - var index = this.humans.indexOf(client); - if (index >= 0) - this.humans.splice(index, 1); - } - } -}; - -TeamZ.prototype.onCellMove = function(x1, y1, cell) { - // Called when a player cell is moved - var team = cell.owner.getTeam(); - var r = cell.getSize(); - - // Find team - for (var i = 0; i < cell.owner.visibleNodes.length; i++) { - // Only collide with player cells - var check = cell.owner.visibleNodes[i]; - - if ((check.getType() != 0) || (cell.owner == check.owner)) { - continue; - } - - if ((this.hasEatenHero(check.owner)) || (this.hasEatenHero(cell.owner))) { - continue; - } - - // Collision with zombies - if (check.owner.getTeam() == 0 || team == 0) { - // Check if in collision range - var collisionDist = check.getSize() + r; // Minimum distance between the 2 cells - if (!cell.simpleCollide(x1, y1, check, collisionDist)) { - // Skip - continue; - } - - // First collision check passed... now more precise checking - dist = cell.getDist(cell.position.x, cell.position.y, check.position.x, check.position.y); - - // Calculations - if (dist < collisionDist) { // Collided - var crazyClient = null; - if (check.owner.getTeam() == 0 && team != 0) { - crazyClient = cell.owner; - } else if (team == 0 && check.owner.getTeam() != 0) { - crazyClient = check.owner; - } - - if (crazyClient != null && !this.isCrazy(crazyClient)) { - crazyClient.crazyTimer = this.crazyDuration; - crazyClient.colorToggle = 0; - this.boostSpeed(crazyClient); - } - - // The moving cell pushes the colliding cell - var newDeltaY = check.position.y - y1; - var newDeltaX = check.position.x - x1; - var newAngle = Math.atan2(newDeltaX, newDeltaY); - - var move = collisionDist - dist; - - check.position.x = check.position.x + (move * Math.sin(newAngle)) >> 0; - check.position.y = check.position.y + (move * Math.cos(newAngle)) >> 0; - } - } - } -}; - -TeamZ.prototype.updateLB = function(gameServer) { - var lb = gameServer.leaderboard; - - if (this.winTeam == 0) { - lb.push('ZOMBIE WINS'); - lb.push('_______________'); - } else if (this.winTeam > 0) { - lb.push('HUMAN WINS'); - lb.push('_______________'); - } - - switch (this.state) { - case GameState.WF_PLAYERS: - lb.push('WAITING FOR'); - lb.push('PLAYERS...'); - lb.push(this.humans.length + '/' + this.minPlayer); - break; - case GameState.WF_START: - lb.push('GAME STARTS IN:'); - var min = (this.gameTimer / 20 / 60) >> 0; - var sec = ((this.gameTimer / 20) >> 0) % 60; - lb.push((min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec); - break; - case GameState.IN_PROGRESS: - var min = (this.gameTimer / 20 / 60) >> 0; - var sec = ((this.gameTimer / 20) >> 0) % 60; - lb.push((min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec); - lb.push('HUMAN: ' + this.humans.length); - lb.push('ZOMBIE: ' + this.zombies.length); - lb.push('_______________'); - - // Loop through all clients - localLB = []; - for (var i = 0; i < gameServer.clients.length; i++) { - if (typeof gameServer.clients[i] == 'undefined' || gameServer.clients[i].playerTracker.team == 0) { - continue; - } - - var player = gameServer.clients[i].playerTracker; - if (player.cells.length <= 0) { - continue; - } - var playerScore = player.getScore(true); - - if (localLB.length == 0) { - // Initial player - localLB.push(player); - continue; - } else if (localLB.length < 6) { - this.leaderboardAddSort(player, localLB); - } else { - // 6 in leaderboard already - if (playerScore > localLB[5].getScore(false)) { - localLB.pop(); - this.leaderboardAddSort(player, localLB); - } - } - } - for (var i = 0; i < localLB.length && lb.length < 10; i++) { - lb.push(localLB[i].getName()); - } - - break; - default: - lb.push('ERROR STATE'); - break; - } - -}; - -// ---------------------------------------------------------------------------- -// Game mode entities: - -// HERO POISON CELL: -function Hero() { - Cell.apply(this, Array.prototype.slice.call(arguments)); - - this.cellType = CellType.HERO; - //this.spiked = 1; - this.color = { - r: 255, - g: 255, - b: 7 - }; - this.mass = 60; -} - -Hero.prototype = new Cell(); - -Hero.prototype.getName = function() { - return 'HERO'; -}; - -Hero.prototype.calcMove = null; - -Hero.prototype.onAdd = function(gameServer) { - gameServer.gameMode.heroes.push(this); -}; - -Hero.prototype.onRemove = function(gameServer) { - var index = gameServer.gameMode.heroes.indexOf(this); - if (index != -1) { - gameServer.gameMode.heroes.splice(index, 1); - } else { - console.log('[Warning] Tried to remove a non existing HERO node!'); - } -}; - -Hero.prototype.feed = function(feeder, gameServer) { - gameServer.removeNode(feeder); - - this.setAngle(feeder.getAngle()); - this.moveEngineTicks = 5; // Amount of times to loop the movement function - this.moveEngineSpeed = 60; - - var index = gameServer.movingNodes.indexOf(this); - if (index == -1) { - gameServer.movingNodes.push(this); - } -}; - -Hero.prototype.onConsume = function(consumer, gameServer) { - // Called when the cell is consumed - var client = consumer.owner; - consumer.addMass(this.mass); // delicious - - if (gameServer.gameMode.isCrazy(client)) { - // Neutralize the Zombie effect - client.cured = true; - } else { - // Become a hero - client.eatenHeroTimer = gameServer.gameMode.heroEffectDuration; - client.heroColorFactor = 0; - - // Merge immediately - for (var i = 0; i < client.cells.length; i++) { - var cell = client.cells[i]; - cell.recombineTicks = 0; - } - - } -}; - -// ---------------------------------------------------------------------------- -// BRAIN CELL: -function Brain() { - Cell.apply(this, Array.prototype.slice.call(arguments)); - - this.cellType = CellType.BRAIN; - //this.spiked = 1; - this.color = { - r: 255, - g: 7, - b: 255 - }; - this.mass = 60; -} - -Brain.prototype = new Cell(); - -Brain.prototype.getName = function() { - return 'BRAIN'; -}; - -Brain.prototype.calcMove = null; - -Brain.prototype.onAdd = function(gameServer) { - gameServer.gameMode.brains.push(this); -}; - -Brain.prototype.onRemove = function(gameServer) { - var index = gameServer.gameMode.brains.indexOf(this); - if (index != -1) { - gameServer.gameMode.brains.splice(index, 1); - } else { - console.log('[Warning] Tried to remove a non existing BRAIN node!'); - } -}; - -Brain.prototype.feed = function(feeder, gameServer) { - gameServer.removeNode(feeder); - - this.setAngle(feeder.getAngle()); - this.moveEngineTicks = 5; // Amount of times to loop the movement function - this.moveEngineSpeed = 60; - - var index = gameServer.movingNodes.indexOf(this); - if (index == -1) { - gameServer.movingNodes.push(this); - } -}; - -Brain.prototype.onConsume = function(consumer, gameServer) { - // Called when the cell is consumed - var client = consumer.owner; - consumer.addMass(this.mass); // yummy! - - client.eatenBrainTimer = gameServer.gameMode.brainEffectDuration; - - // Boost speed - gameServer.gameMode.boostSpeed(client); -}; +// TODO: fix this game mode has outdated code and probably will not works +var Mode = require('./Mode.js'); +var Cell = require('../entity/Cell.js'); +var Entity = require('../entity'); +var Virus = require('../entity/Virus.js'); + +var GameServer = null; // represent GameServer Type +var GS_getRandomColor = null; // backup getRandomColor function of GameServer type +var GS_getNearestVirus = null; +var GS_getCellsInRange = null; +var GS_splitCells = null; +var GS_newCellVirused = null; +var Virus_onEaten = Virus.prototype.onEaten; + +var GameState = { + WF_PLAYERS: 0, + WF_START: 1, + IN_PROGRESS: 2 +}; + +// new Cell Type IDs of HERO and BRAIN are calculated based on Game Mode ID +var CellType = { + PLAYER: 0, + FOOD: 1, + VIRUS: 2, + EJECTED_MASS: 3, + HERO: 130, + BRAIN: 131 +}; + +var localLB = []; + +function TeamZ() { + Mode.apply(this, Array.prototype.slice.call(arguments)); + + this.ID = 13; + this.name = 'Zombie Team'; + this.packetLB = 48; + this.haveTeams = true; + + // configurations: + this.minPlayer = 2; // game is auto started if there are at least 2 players + this.gameDuration = 18000; // ticks, 1 tick = 50 ms (20 ticks = 1 s) + this.warmUpDuration = 600; // ticks, time to wait between games + this.crazyDuration = 200; // ticks + this.heroEffectDuration = 1000; // ticks + this.brainEffectDuration = 200; // ticks + this.spawnBrainInterval = 1200; // ticks + this.spawnHeroInterval = 600; // ticks + this.defaultColor = { + r: 0x9b, + g: 0x30, + b: 0xff + }; + + this.colorFactorStep = 5; + this.colorLower = 50; // Min 0 + this.colorUpper = 225; // Max 255 + this.maxBrain = -1; // set this param to any negative number to keep the number of brains not exceed number of humans + this.maxHero = 4; // set this param to any negative number to keep the number of heroes not exceed number of zombies + + // game mode data: + this.state = GameState.WF_PLAYERS; + this.winTeam = -1; + this.gameTimer = 0; + this.zombies = []; // the clients of zombie players + this.humans = []; // the clients of human players + + this.heroes = []; + this.brains = []; + + this.spawnHeroTimer = 0; + this.spawnBrainTimer = 0; +} + +module.exports = TeamZ; +TeamZ.prototype = new Mode(); + +// Gamemode Specific Functions + +TeamZ.prototype.createZColorFactor = function (client) { + client.zColorFactor = (Math.random() * (this.colorUpper - this.colorLower + 1)) >> 0 + this.colorLower; + client.zColorIncr = true; // color will be increased if TRUE - otherwise it will be decreased. +}; + +TeamZ.prototype.nextZColorFactor = function (client) { + if (client.zColorIncr == true) { + if (client.zColorFactor + this.colorFactorStep >= this.colorUpper) { + client.zColorFactor = this.colorUpper; + client.zColorIncr = false; + } else { + client.zColorFactor += this.colorFactorStep; + } + } else { + if (client.zColorFactor - this.colorFactorStep <= this.colorLower) { + client.zColorFactor = this.colorLower; + client.zColorIncr = true; + } else { + client.zColorFactor -= this.colorFactorStep; + } + } +}; + +TeamZ.prototype.updateZColor = function (client, mask) { + var color = { + r: (mask & 0x4) > 0 ? client.zColorFactor : 7, + g: (mask & 0x2) > 0 ? client.zColorFactor : 7, + b: (mask & 0x1) > 0 ? client.zColorFactor : 7 + }; + client.setColor(color); + for (var i = 0; i < client.cells.length; i++) { + var cell = client.cells[i]; + cell.setColor(color); + } +}; + +TeamZ.prototype.isCrazy = function (client) { + return (typeof (client.crazyTimer) != 'undefined' && client.crazyTimer > 0 && client.team > 0); +}; + +TeamZ.prototype.hasEatenHero = function (client) { + return (typeof (client.eatenHeroTimer) != 'undefined' && client.eatenHeroTimer > 0); +}; + +TeamZ.prototype.hasEatenBrain = function (client) { + return (typeof (client.eatenBrainTimer) != 'undefined' && client.eatenBrainTimer > 0); +}; + +TeamZ.prototype.spawnDrug = function (gameServer, cell) { // spawn HERO or BRAIN + var max = 0; + var proceedNext = false; + if (cell.getType() == CellType.HERO) { + max = this.maxHero < 0 ? this.zombies.length : this.maxHero; + proceedNext = this.heroes.length < max; + } else if (cell.getType() == CellType.BRAIN) { + max = this.maxBrain < 0 ? this.humans.length : this.maxBrain; + proceedNext = this.brains.length < max; + } + if (proceedNext) { + var pos = gameServer.getRandomPosition(); + + // Check for players + var size = cell.getSize(); + var bound = { + minx: pos.x - size, + miny: pos.y - size, + maxx: pos.x + size, + maxy: pos.y + size + }; + if (gameServer.quadTree.any(bound, function (item) { return item.cell.cellType == 0; })) { + // FAILED because of collision + return false; + } + cell.setPosition(pos); + gameServer.addNode(cell); + return true; // SUCCESS with spawn + } + return true; // SUCCESS without spawn +}; + +// Call to change a human client to a zombie +TeamZ.prototype.turnToZombie = function (client) { + client.team = 0; // team Z + this.createZColorFactor(client); + this.updateZColor(client, 0x7); // Gray + + // remove from human list + var index = this.humans.indexOf(client); + if (index >= 0) { + this.humans.splice(index, 1); + } + + // add to zombie list + this.zombies.push(client); +}; + +TeamZ.prototype.boostSpeedCell = function (cell) { + if (typeof cell.originalSpeed == 'undefined' || cell.originalSpeed == null) { + cell.originalSpeed = cell.getSpeed; + cell.getSpeed = function () { + return 2 * this.originalSpeed(); + }; + } +}; + +TeamZ.prototype.boostSpeed = function (client) { + for (var i = 0; i < client.cells.length; i++) { + var cell = client.cells[i]; + if (typeof cell == 'undefined') + continue; + this.boostSpeedCell(cell); + } +}; + +TeamZ.prototype.resetSpeedCell = function (cell) { + if (typeof cell.originalSpeed != 'undefined' && cell.originalSpeed != null) { + cell.getSpeed = cell.originalSpeed; + cell.originalSpeed = null; + } +}; + +TeamZ.prototype.resetSpeed = function (client) { + for (var i = 0; i < client.cells.length; i++) { + var cell = client.cells[i]; + if (typeof cell == 'undefined') + continue; + this.resetSpeedCell(cell); + } +}; + +TeamZ.prototype.startGame = function (gameServer) { + for (var i = 0; i < this.humans.length; i++) { + var client = this.humans[i]; + client.team = client.pID; + client.crazyTimer = 0; + client.eatenHeroTimer = 0; + client.eatenBrainTimer = 0; + client.setColor(gameServer.getRandomColor()); + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + if (cell) { + cell.setColor(client.getColor()); + cell.setSize(gameServer.config.playerMinSize); + this.resetSpeedCell(cell); + } + } + } + + // Select random human to be the zombie + var zombie = this.humans[(Math.random() * this.humans.length) >> 0]; + this.turnToZombie(zombie); + + this.winTeam = -1; + this.state = GameState.IN_PROGRESS; + this.gameTimer = this.gameDuration; +}; + +TeamZ.prototype.endGame = function (gameServer) { + // reset game + for (var i = 0; i < this.zombies.length; i++) { + var client = this.zombies[i]; + var index = this.humans.indexOf(client); + if (index < 0) { + this.humans.push(client); + } + } + this.zombies = []; + this.spawnHeroTimer = 0; + this.spawnBrainTimer = 0; + localLB = []; // reset leader board + + for (var i = 0; i < this.humans.length; i++) { + var client = this.humans[i]; + client.color = this.defaultColor; + client.team = 1; + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + cell.setColor(this.defaultColor); + } + } + + this.state = GameState.WF_PLAYERS; + this.gameTimer = 0; +}; + +TeamZ.prototype.leaderboardAddSort = function (player, leaderboard) { + // Adds the player and sorts the leaderboard + var len = leaderboard.length - 1; + var loop = true; + + while ((len >= 0) && (loop)) { + // Start from the bottom of the leaderboard + if (player.getScore() <= leaderboard[len].getScore()) { + leaderboard.splice(len + 1, 0, player); + loop = false; // End the loop if a spot is found + } + len--; + } + if (loop) { + // Add to top of the list because no spots were found + leaderboard.splice(0, 0, player); + } +}; + +// Override + +TeamZ.prototype.onServerInit = function (gameServer) { + // Called when the server starts + gameServer.run = true; + + // Overwrite some server functions: + GameServer = require('../GameServer.js'); + GS_getRandomColor = GameServer.prototype.getRandomColor; // backup + GS_getNearestVirus = GameServer.prototype.getNearestVirus; + GS_getCellsInRange = GameServer.prototype.getCellsInRange; + GS_splitCells = GameServer.prototype.splitCells; + GS_newCellVirused = GameServer.prototype.newCellVirused; + + //OVERWRITE GLOBAL FUNCTIONs to adapt Zombie Team mode + + GameServer.prototype.getRandomColor = function () { + var colorRGB = [0xFF, 0x07, (Math.random() * 256) >> 0]; + colorRGB.sort(function () { + return 0.5 - Math.random(); + }); + return { + r: colorRGB[0], + b: colorRGB[1], + g: colorRGB[2] + }; + }; + + GameServer.prototype.getNearestVirus = function (cell) { + // More like getNearbyVirus + var virus = null; + + // loop through all heroes + for (var i = 0; i < this.gameMode.heroes.length; i++) { + var check = this.gameMode.heroes[i]; + if (typeof check === 'undefined') { + continue; + } + if (this.checkCellCollision(cell, check) == null) { + continue; + } + virus = check; + break; + } + if (virus != null) + return virus; + + // loop through all brains + for (var i = 0; i < this.gameMode.brains.length; i++) { + var check = this.gameMode.brains[i]; + if (typeof check === 'undefined') { + continue; + } + if (this.checkCellCollision(cell, check) == null) { + continue; + } + virus = check; + break; + } + + if (virus != null) + return virus; + + // Call base: + // Loop through all viruses on the map. There is probably a more efficient way of doing this but whatever + var len = this.nodesVirus.length; + for (var i = 0; i < len; i++) { + var check = this.nodesVirus[i]; + + if (typeof check === 'undefined') { + continue; + } + + if (this.checkCellCollision(cell, check) == null) { + continue; + } + + // Add to list of cells nearby + virus = check; + break; // stop checking when a virus found + } + return virus; + }; + + // this is almost same to the legacy function + GameServer.prototype.getCellsInRange = function (cell) { + var list = new Array(); + + if (this.gameMode.state != GameState.IN_PROGRESS) + return list; + + var squareR = cell.getSizeSquared(); // Get cell squared radius + + // Loop through all cells that are visible to the cell. There is probably a more efficient way of doing this but whatever + var len = cell.owner.visibleNodes.length; + for (var i = 0; i < len; i++) { + var check = cell.owner.visibleNodes[i]; + + if (typeof check === 'undefined') { + continue; + } + + // if something already collided with this cell, don't check for other collisions + if (check.isRemoved) { + continue; + } + + // HERO and BRAIN checking + if (cell.owner.getTeam() == 0) { + // Z team + if (check.getType() == CellType.HERO) + continue; + } else { + // H team + if (check.getType() == CellType.BRAIN) + continue; + } + + // Can't eat itself + if (cell.nodeId == check.nodeId) { + continue; + } + + // Can't eat cells that have collision turned off + if ((cell.owner == check.owner) && (cell.ignoreCollision)) { + continue; + } + + // AABB Collision + if (gameServer.checkCellCollision(cell, check) == null) { + continue; + } + + // Cell type check - Cell must be bigger than this number times the mass of the cell being eaten + var multiplier = 1.25; + + switch (check.getType()) { + case 1:// Food cell + list.push(check); + check.isRemoved = true; // skip future collision checks for this food + continue; + case 2:// Virus + multiplier = 1.33; + break; + case 0:// Players + // Can't eat self if it's not time to recombine yet + if (check.owner == cell.owner) { + if (!cell.canRemrege() || !check.canRemerge()) { + continue; + } + + multiplier = 1.00; + } + + // Can't eat team members + if (this.gameMode.haveTeams) { + if (!check.owner) { // Error check + continue; + } + + if ((check.owner != cell.owner) && (check.owner.getTeam() == cell.owner.getTeam())) { + continue; + } + } + break; + default: + break; + } + + // Make sure the cell is big enough to be eaten. + if ((check.getMass() * multiplier) > cell.getMass()) { + continue; + } + + // Eating range + var xs = Math.pow(check.position.x - cell.position.x, 2); + var ys = Math.pow(check.position.y - cell.position.y, 2); + var dist = Math.sqrt(xs + ys); + + var eatingRange = cell.getSize() - check.getSize() / Math.PI; // Eating range = radius of eating cell + 40% of the radius of the cell being eaten + if (dist > eatingRange) { + // Not in eating range + continue; + } + + // Add to list of cells nearby + list.push(check); + + // Something is about to eat this cell; no need to check for other collisions with it + check.isRemoved = true; + } + return list; + }; + + // this is almost same to the legacy function + GameServer.prototype.splitCells = function (client) { + var len = client.cells.length; + for (var i = 0; i < len; i++) { + if (client.cells.length >= this.config.playerMaxCells) { + // Player cell limit + continue; + } + + var cell = client.cells[i]; + if (!cell) { + continue; + } + + if (cell.getSize() < this.config.playerMinSplitSize) { + continue; + } + + // Get angle + var deltaY = client.mouse.y - cell.position.y; + var deltaX = client.mouse.x - cell.position.x; + var angle = Math.atan2(deltaX, deltaY); + + // Get starting position + var size = cell.getSize() / 2; + var startPos = { + x: cell.position.x + (size * Math.sin(angle)), + y: cell.position.y + (size * Math.cos(angle)) + }; + // Calculate mass and speed of splitting cell + var splitSpeed = cell.getSpeed() * 6; + var newSize = cell.getSplitSize(); + cell.setSize(newSize); + // Create cell + var split = new Entity.PlayerCell(this, client, startPos, newSize); + // TODO: check distance + split.setBoost(splitSpeed * 32, angle); + + // boost speed if zombie eats brain + if (this.gameMode.hasEatenBrain(client) || this.gameMode.isCrazy(client)) { + this.gameMode.boostSpeedCell(split); + } + // gain effect if human eat hero + else if (this.gameMode.hasEatenHero(client)) { + // fix "unable to split" bug: cell can be merged after finish moving (2nd param in setMoveEngineData) + //split.recombineTicks = 2; // main-ticks, 1 main-tick = 1 s + //TODO: fix? + } + this.addNode(split); + } + }; + + // this function is almost same to the legacy + GameServer.prototype.newCellVirused = function (client, parent, angle, mass, speed) { + // Starting position + var startPos = { + x: parent.position.x, + y: parent.position.y + }; + + var size = Math.sqrt(mass); + // Create cell + newCell = new Entity.PlayerCell(this, client, startPos, size); + // TODO: check distance + newCell.setBoost(speed * 10, angle); + //newCell.setAngle(angle); + //newCell.setMoveEngineData(speed, 10); + newCell.ignoreCollision = true; // Turn off collision + + // boost speed if zombie eats brain + if (this.gameMode.hasEatenBrain(client) || this.gameMode.isCrazy(client)) { + this.gameMode.boostSpeedCell(newCell); + } + // gain effect if human eat hero + else if (this.gameMode.hasEatenHero(client)) { + // fix "unable to split" bug + //newCell.recombineTicks = 1; + // TODO: fix? + } + + // Add to moving cells list + this.addNode(newCell); + }; + + Virus.prototype.onEaten = function (consumer) { + var client = consumer.owner; + var maxSplits = Math.floor(consumer.getMass() / 16) - 1; // Maximum amount of splits + var numSplits = this.gameServer.config.playerMaxCells - client.cells.length; // Get number of splits + numSplits = Math.min(numSplits, maxSplits); + var splitMass = Math.min(consumer.getMass() / (numSplits + 1), 36); // Maximum size of new splits + + // Cell cannot split any further + if (numSplits <= 0) { + return; + } + + // Big cells will split into cells larger than 36 mass (1/4 of their mass) + var bigSplits = 0; + var endMass = consumer.getMass() - (numSplits * splitMass); + if ((endMass > 300) && (numSplits > 0)) { + bigSplits++; + numSplits--; + } + if ((endMass > 1200) && (numSplits > 0)) { + bigSplits++; + numSplits--; + } + if ((endMass > 3000) && (numSplits > 0)) { + bigSplits++; + numSplits--; + } + + // Splitting + var angle = 0; // Starting angle + for (var k = 0; k < numSplits; k++) { + angle += 6 / numSplits; // Get directions of splitting cells + this.gameServer.newCellVirused(client, consumer, angle, splitMass, 150); + consumer.setSize(Math.sqrt(consumer.getSize() * consumer.getSize() - splitMass * splitMass)); + } + + for (var k = 0; k < bigSplits; k++) { + angle = Math.random() * 6.28; // Random directions + splitMass = consumer.getMass() / 4; + this.gameServer.newCellVirused(client, consumer, angle, splitMass, 20); + consumer.setSize(Math.sqrt(consumer.getSize() * consumer.getSize() - splitMass * splitMass)); + } + + if (this.gameServer.gameMode.hasEatenHero(client)) { + //consumer.recombineTicks = 0; + // TODO: fix? + } + }; + + // Handle "gamemode" command: + for (var i = 0; i < this.gameServer.clients.length; i++) { + var client = this.gameServer.clients[i].playerTracker; + if (!client) + continue; + + if (client.cells.length > 0) { + client.eatenBrainTimer = 0; + client.eatenHeroTimer = 0; + client.crazyTimer = 0; + client.setColor(this.defaultColor); + client.team = 1; + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + cell.setColor(this.defaultColor); + } + this.humans.push(client); + } + } +}; + +TeamZ.prototype.onChange = function (gameServer) { + // Called when someone changes the gamemode via console commands + // remove Brain and Hero + for (var i = 0; this.brains.length; i++) { + var node = this.brains[i]; + gameServer.removeNode(node); + } + for (var i = 0; this.heroes.length; i++) { + var node = this.heroes[i]; + gameServer.removeNode(node); + } + + // discard all boost: + for (var i = 0; i < this.humans.length; i++) { + var client = this.humans[i]; + if (this.isCrazy(client)) { + this.resetSpeed(client); + } + } + for (var i = 0; i < this.zombies.length; i++) { + var client = this.zombies[i]; + if (this.hasEatenBrain(client)) { + this.resetSpeed(client); + } + } + + // revert to default: + GameServer.prototype.getRandomColor = GS_getRandomColor; + GameServer.prototype.getNearestVirus = GS_getNearestVirus; + GameServer.prototype.getCellsInRange = GS_getCellsInRange; + GameServer.prototype.splitCells = GS_splitCells; + GameServer.prototype.newCellVirused = GS_newCellVirused; + Virus.prototype.onEaten = Virus_onEaten; +}; + +TeamZ.prototype.onTick = function (gameServer) { + // Called on every game tick + + switch (this.state) { + case GameState.WF_PLAYERS: + if (this.humans.length >= this.minPlayer) { + this.state = GameState.WF_START; + this.gameTimer = this.warmUpDuration; + } + break; + case GameState.WF_START: + this.gameTimer--; + if (this.gameTimer == 0) { + if (this.humans.length >= this.minPlayer) { + // proceed: + this.startGame(gameServer); + } else { + // back to previous state: + this.state = GameState.WF_PLAYERS; + } + } + break; + case GameState.IN_PROGRESS: + this.gameTimer--; + if (this.gameTimer == 0) { + // human wins + this.winTeam = 1; + } else { + if (this.humans.length == 0) { // no human left + // zombie wins + this.winTeam = 0; + } else if (this.zombies.length == 0) { // no zombie left + // human wins + this.winTeam = 1; + } + } + + if (this.winTeam >= 0) { + this.endGame(gameServer); + } + + break; + } + + // change color of zombies + for (var i = 0; i < this.zombies.length; i++) { + var client = this.zombies[i]; + this.nextZColorFactor(client); + + if (this.hasEatenBrain(client)) { + client.eatenBrainTimer--; + + if (client.eatenBrainTimer > 0) { + this.updateZColor(client, 0x5); // Pink + continue; + } else { + // reset speed: + this.resetSpeed(client); + } + } + + this.updateZColor(client, 0x7); // Gray + } + + for (var i = 0; i < this.humans.length; i++) { + var client = this.humans[i]; + if (this.isCrazy(client)) { + client.crazyTimer--; + if (client.crazyTimer == 0) { + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + // reset speed: + this.resetSpeedCell(cell); + + // reset color: + if (client.cured == true) + cell.setColor(client.getColor()); + } + + if (client.cured == true) { + client.cured = false; // reset + } else { + // turn player to zombie + this.turnToZombie(client); + continue; + } + } else { + client.colorToggle++; + if (client.colorToggle % 10 == 0) { + var blinkColor = null; + + if (client.colorToggle == 20) { + blinkColor = client.getColor(); + client.colorToggle = 0; + } else { + if (client.cured == true) { + blinkColor = { + r: 255, + g: 255, + b: 7 + }; // Yellow + } else { + blinkColor = { + r: 75, + g: 75, + b: 75 + }; // Gray + } + } + + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + cell.setColor(blinkColor); + } + } + } + } else if (this.hasEatenHero(client)) { + client.eatenHeroTimer--; + var color = null; + if (client.eatenHeroTimer > 0) { + client.heroColorFactor = (client.heroColorFactor + 5) % 401; + if (client.heroColorFactor <= 200) { + color = { + r: 255, + g: 255, + b: client.heroColorFactor + }; // Yellow scheme + } else { + color = { + r: 255, + g: 255, + b: 400 - client.heroColorFactor + }; // Yellow scheme + } + } else { + color = client.getColor(); // reset + } + + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + cell.setColor(color); + } + } + } + + // check timer to spawn Hero: + this.spawnHeroTimer++; + if (this.spawnHeroTimer >= this.spawnHeroInterval) { + this.spawnHeroTimer = 0; + var cell = new Hero(gameServer.getNextNodeId(), null); + while (!this.spawnDrug(gameServer, cell)) ; // collision detect algorithm needs enhancement + } + + // check timer to spawn Brain: + this.spawnBrainTimer++; + if (this.spawnBrainTimer >= this.spawnBrainInterval) { + this.spawnBrainTimer = 0; + var cell = new Brain(gameServer.getNextNodeId(), null); + while (!this.spawnDrug(gameServer, cell)) ; // collision detect algorithm needs enhancement + } +}; + +TeamZ.prototype.onCellAdd = function (cell) { + // Called when a player cell is added + var client = cell.owner; + if (client.cells.length == 1) { // first cell + client.team = client.pID; + client.setColor(cell.getColor()); + client.eatenBrainTimer = 0; + client.eatenHeroTimer = 0; + client.crazyTimer = 0; + this.humans.push(client); + + if (this.state == GameState.IN_PROGRESS) { + this.turnToZombie(client); + } else { + client.setColor(this.defaultColor); + cell.setColor(this.defaultColor); + client.team = 1; // game not started yet + } + } +}; + +TeamZ.prototype.onCellRemove = function (cell) { + // Called when a player cell is removed + var client = cell.owner; + if (client.cells.length == 0) { // last cell + if (client.getTeam() == 0) { + // Z team + var index = this.zombies.indexOf(client); + if (index >= 0) + this.zombies.splice(index, 1); + } else { + // H team + var index = this.humans.indexOf(client); + if (index >= 0) + this.humans.splice(index, 1); + } + } +}; + +// TODO: remove it (move physics is managed by GameServer) +TeamZ.prototype.onCellMove = function (x1, y1, cell) { + // Called when a player cell is moved + var team = cell.owner.getTeam(); + var r = cell.getSize(); + + // Find team + for (var i = 0; i < cell.owner.visibleNodes.length; i++) { + // Only collide with player cells + var check = cell.owner.visibleNodes[i]; + + if ((check.getType() != 0) || (cell.owner == check.owner)) { + continue; + } + + if ((this.hasEatenHero(check.owner)) || (this.hasEatenHero(cell.owner))) { + continue; + } + + // Collision with zombies + if (check.owner.getTeam() == 0 || team == 0) { + // Check if in collision range + var collisionDist = check.getSize() + r; // Minimum distance between the 2 cells + if (!cell.simpleCollide(x1, y1, check, collisionDist)) { + // Skip + continue; + } + + // First collision check passed... now more precise checking + dist = cell.getDist(cell.position.x, cell.position.y, check.position.x, check.position.y); + + // Calculations + if (dist < collisionDist) { // Collided + var crazyClient = null; + if (check.owner.getTeam() == 0 && team != 0) { + crazyClient = cell.owner; + } else if (team == 0 && check.owner.getTeam() != 0) { + crazyClient = check.owner; + } + + if (crazyClient != null && !this.isCrazy(crazyClient)) { + crazyClient.crazyTimer = this.crazyDuration; + crazyClient.colorToggle = 0; + this.boostSpeed(crazyClient); + } + + // The moving cell pushes the colliding cell + var newDeltaY = check.position.y - y1; + var newDeltaX = check.position.x - x1; + var newAngle = Math.atan2(newDeltaX, newDeltaY); + + var move = collisionDist - dist; + + check.setPosition({ + x: check.position.x + (move * Math.sin(newAngle)) >> 0, + y: check.position.y + (move * Math.cos(newAngle)) >> 0 + }); + } + } + } +}; + +TeamZ.prototype.updateLB = function (gameServer) { + gameServer.leaderboardType = this.packetLB; + var lb = gameServer.leaderboard; + + if (this.winTeam == 0) { + lb.push('ZOMBIE WINS'); + lb.push('_______________'); + } else if (this.winTeam > 0) { + lb.push('HUMAN WINS'); + lb.push('_______________'); + } + + switch (this.state) { + case GameState.WF_PLAYERS: + lb.push('WAITING FOR'); + lb.push('PLAYERS...'); + lb.push(this.humans.length + '/' + this.minPlayer); + break; + case GameState.WF_START: + lb.push('GAME STARTS IN:'); + var min = (this.gameTimer / 20 / 60) >> 0; + var sec = ((this.gameTimer / 20) >> 0) % 60; + lb.push((min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec); + break; + case GameState.IN_PROGRESS: + var min = (this.gameTimer / 20 / 60) >> 0; + var sec = ((this.gameTimer / 20) >> 0) % 60; + lb.push((min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec); + lb.push('HUMAN: ' + this.humans.length); + lb.push('ZOMBIE: ' + this.zombies.length); + lb.push('_______________'); + + // Loop through all clients + localLB = []; + for (var i = 0; i < gameServer.clients.length; i++) { + if (typeof gameServer.clients[i] == 'undefined' || gameServer.clients[i].playerTracker.team == 0) { + continue; + } + + var player = gameServer.clients[i].playerTracker; + if (player.cells.length <= 0) { + continue; + } + var playerScore = player.getScore(); + + if (localLB.length == 0) { + // Initial player + localLB.push(player); + continue; + } else if (localLB.length < 6) { + this.leaderboardAddSort(player, localLB); + } else { + // 6 in leaderboard already + if (playerScore > localLB[5].getScore()) { + localLB.pop(); + this.leaderboardAddSort(player, localLB); + } + } + } + for (var i = 0; i < localLB.length && lb.length < 10; i++) { + lb.push(localLB[i].getName()); + } + + break; + default: + lb.push('ERROR STATE'); + break; + } + +}; + +// ---------------------------------------------------------------------------- +// Game mode entities: + +// HERO POISON CELL: +function Hero() { + Cell.apply(this, Array.prototype.slice.call(arguments)); + + this.cellType = CellType.HERO; + //this.isSpiked = true; + this.setColor({ r: 255, g: 255, b: 7 }); + this.setSize(78); +} + +Hero.prototype = new Cell(); + +Hero.prototype.getName = function () { + return 'HERO'; +}; + +Hero.prototype.calcMove = null; + +Hero.prototype.onAdd = function (gameServer) { + gameServer.gameMode.heroes.push(this); +}; + +Hero.prototype.onRemove = function (gameServer) { + var index = gameServer.gameMode.heroes.indexOf(this); + if (index != -1) { + gameServer.gameMode.heroes.splice(index, 1); + } else { + console.log('[Warning] Tried to remove a non existing HERO node!'); + } +}; + +Hero.prototype.feed = function (feeder, gameServer) { + gameServer.removeNode(feeder); + + // TODO: check distance + this.setBoost(60 * 5, feeder.getAngle()); + //this.setAngle(feeder.getAngle()); + //this.moveEngineTicks = 5; // Amount of times to loop the movement function + //this.moveEngineSpeed = 60; + + var index = gameServer.movingNodes.indexOf(this); + if (index == -1) { + gameServer.movingNodes.push(this); + } +}; + +Hero.prototype.onEaten = function (consumer) { + // Called when the cell is consumed + var client = consumer.owner; + + // delicious + + if (this.gameServer.gameMode.isCrazy(client)) { + // Neutralize the Zombie effect + client.cured = true; + } else { + // Become a hero + client.eatenHeroTimer = this.gameServer.gameMode.heroEffectDuration; + client.heroColorFactor = 0; + + // Merge immediately + //for (var i = 0; i < client.cells.length; i++) { + // var cell = client.cells[i]; + // cell.recombineTicks = 0; + //} + // TODO: fix? + + } +}; + +// ---------------------------------------------------------------------------- +// BRAIN CELL: +function Brain() { + Cell.apply(this, Array.prototype.slice.call(arguments)); + + this.cellType = CellType.BRAIN; + //this.isSpiked = true; + this.setColor({ r: 255, g: 7, b: 255 }); + this.setSize(78); +} + +Brain.prototype = new Cell(); + +Brain.prototype.getName = function () { + return 'BRAIN'; +}; + +Brain.prototype.calcMove = null; + +Brain.prototype.onAdd = function (gameServer) { + gameServer.gameMode.brains.push(this); +}; + +Brain.prototype.onRemove = function (gameServer) { + var index = gameServer.gameMode.brains.indexOf(this); + if (index != -1) { + gameServer.gameMode.brains.splice(index, 1); + } else { + console.log('[Warning] Tried to remove a non existing BRAIN node!'); + } +}; + +Brain.prototype.feed = function (feeder, gameServer) { + gameServer.removeNode(feeder); + + // TODO: check distance + this.setBoost(60 * 5, feeder.getAngle()); + //this.setAngle(feeder.getAngle()); + //this.moveEngineTicks = 5; // Amount of times to loop the movement function + //this.moveEngineSpeed = 60; + + var index = gameServer.movingNodes.indexOf(this); + if (index == -1) { + gameServer.movingNodes.push(this); + } +}; + +Brain.prototype.onEaten = function (consumer) { + // Called when the cell is consumed + var client = consumer.owner; + + // yummy! + + client.eatenBrainTimer = this.gameServer.gameMode.brainEffectDuration; + + // Boost speed + this.gameServer.gameMode.boostSpeed(client); +}; diff --git a/src/gamemodes/Teams.js b/src/gamemodes/Teams.js index 8362525e3..816e4fa17 100644 --- a/src/gamemodes/Teams.js +++ b/src/gamemodes/Teams.js @@ -1,154 +1,153 @@ -var Mode = require('./Mode'); - -function Teams() { - Mode.apply(this, Array.prototype.slice.call(arguments)); - - this.ID = 1; - this.name = "Teams"; - this.decayMod = 1.5; - this.packetLB = 50; - this.haveTeams = true; - this.colorFuzziness = 32; - - // Special - this.teamAmount = 3; // Amount of teams. Having more than 3 teams will cause the leaderboard to work incorrectly (client issue). - this.colors = [{ - 'r': 223, - 'g': 0, - 'b': 0 - }, { - 'r': 0, - 'g': 223, - 'b': 0 - }, { - 'r': 0, - 'g': 0, - 'b': 223 - }, ]; // Make sure you add extra colors here if you wish to increase the team amount [Default colors are: Red, Green, Blue] - this.nodes = []; // Teams -} - -module.exports = Teams; -Teams.prototype = new Mode(); - -//Gamemode Specific Functions - -Teams.prototype.fuzzColorComponent = function(component) { - component += Math.random() * this.colorFuzziness >> 0; - return component; -}; - -Teams.prototype.getTeamColor = function(team) { - var color = this.colors[team]; - return { - r: this.fuzzColorComponent(color.r), - b: this.fuzzColorComponent(color.b), - g: this.fuzzColorComponent(color.g) - }; -}; - -// Override - -Teams.prototype.onPlayerSpawn = function(gameServer, player) { - // Random color based on team - player.color = this.getTeamColor(player.team); - // Spawn player - gameServer.spawnPlayer(player); -}; - -Teams.prototype.onServerInit = function(gameServer) { - // Set up teams - for (var i = 0; i < this.teamAmount; i++) { - this.nodes[i] = []; - } - - // migrate current players to team mode - for (var i = 0; i < gameServer.clients.length; i++) { - var client = gameServer.clients[i].playerTracker; - this.onPlayerInit(client); - client.color = this.getTeamColor(client.team); - for (var j = 0; j < client.cells.length; j++) { - var cell = client.cells[j]; - cell.setColor(client.color); - this.nodes[client.team].push(cell); - } - } -}; - -Teams.prototype.onPlayerInit = function(player) { - // Get random team - player.team = Math.floor(Math.random() * this.teamAmount); -}; - -Teams.prototype.onCellAdd = function(cell) { - // Add to team list - this.nodes[cell.owner.getTeam()].push(cell); -}; - -Teams.prototype.onCellRemove = function(cell) { - // Remove from team list - var index = this.nodes[cell.owner.getTeam()].indexOf(cell); - if (index != -1) { - this.nodes[cell.owner.getTeam()].splice(index, 1); - } -}; - -Teams.prototype.onCellMove = function(cell, gameServer) { - var team = cell.owner.getTeam(); - var r = cell.getSize(); - - // Find team - for (var i = 0; i < cell.owner.visibleNodes.length; i++) { - // Only collide with player cells - var check = cell.owner.visibleNodes[i]; - - if ((check.getType() != 0) || (cell.owner == check.owner)) { - continue; - } - - // Collision with teammates - if (check.owner.getTeam() == team) { - var calcInfo = gameServer.checkCellCollision(cell, check); // Calculation info - - // Further calculations - if (calcInfo.collided) { // Collided - // Cell with collision restore ticks on should not collide - if (cell.collisionRestoreTicks > 0 || check.collisionRestoreTicks > 0) continue; - - // Call gameserver's function to collide cells - gameServer.cellCollision(cell, check, calcInfo); - } - } - } -}; - -Teams.prototype.updateLB = function(gameServer) { - var total = 0; - var teamMass = []; - // Get mass - for (var i = 0; i < this.teamAmount; i++) { - // Set starting mass - teamMass[i] = 0; - - // Loop through cells - for (var j = 0; j < this.nodes[i].length; j++) { - var cell = this.nodes[i][j]; - - if (!cell) { - continue; - } - - teamMass[i] += cell.mass; - total += cell.mass; - } - } - // Calc percentage - for (var i = 0; i < this.teamAmount; i++) { - // No players - if (total <= 0) { - continue; - } - - gameServer.leaderboard[i] = teamMass[i] / total; - } -}; +var Mode = require('./Mode'); + +function Teams() { + Mode.apply(this, Array.prototype.slice.call(arguments)); + + this.ID = 1; + this.name = "Teams"; + this.decayMod = 1.5; + this.packetLB = 50; + this.haveTeams = true; + this.colorFuzziness = 32; + + // Special + this.teamAmount = 3; // Amount of teams. Having more than 3 teams will cause the leaderboard to work incorrectly (client issue). + this.colors = [{ + 'r': 223, + 'g': 0, + 'b': 0 + }, { + 'r': 0, + 'g': 223, + 'b': 0 + }, { + 'r': 0, + 'g': 0, + 'b': 223 + },]; // Make sure you add extra colors here if you wish to increase the team amount [Default colors are: Red, Green, Blue] + this.nodes = []; // Teams +} + +module.exports = Teams; +Teams.prototype = new Mode(); + +//Gamemode Specific Functions + +Teams.prototype.fuzzColorComponent = function (component) { + component += Math.random() * this.colorFuzziness >> 0; + return component; +}; + +Teams.prototype.getTeamColor = function (team) { + var color = this.colors[team]; + return { + r: this.fuzzColorComponent(color.r), + b: this.fuzzColorComponent(color.b), + g: this.fuzzColorComponent(color.g) + }; +}; + +// Override + +Teams.prototype.onPlayerSpawn = function (gameServer, player) { + // Random color based on team + player.setColor(this.getTeamColor(player.team)); + // Spawn player + gameServer.spawnPlayer(player); +}; + +Teams.prototype.onServerInit = function (gameServer) { + // Set up teams + for (var i = 0; i < this.teamAmount; i++) { + this.nodes[i] = []; + } + + // migrate current players to team mode + for (var i = 0; i < gameServer.clients.length; i++) { + var client = gameServer.clients[i].playerTracker; + this.onPlayerInit(client); + client.setColor(this.getTeamColor(client.team)); + for (var j = 0; j < client.cells.length; j++) { + var cell = client.cells[j]; + cell.setColor(client.getColor()); + this.nodes[client.team].push(cell); + } + } +}; + +Teams.prototype.onPlayerInit = function (player) { + // Get random team + player.team = Math.floor(Math.random() * this.teamAmount); +}; + +Teams.prototype.onCellAdd = function (cell) { + // Add to team list + this.nodes[cell.owner.getTeam()].push(cell); +}; + +Teams.prototype.onCellRemove = function (cell) { + // Remove from team list + var index = this.nodes[cell.owner.getTeam()].indexOf(cell); + if (index != -1) { + this.nodes[cell.owner.getTeam()].splice(index, 1); + } +}; + +Teams.prototype.onCellMove = function (cell, gameServer) { + var team = cell.owner.getTeam(); + var r = cell.getSize(); + + // Find team + for (var i = 0; i < cell.owner.visibleNodes.length; i++) { + // Only collide with player cells + var check = cell.owner.visibleNodes[i]; + + if ((check.getType() != 0) || (cell.owner == check.owner)) { + continue; + } + + // Collision with teammates + if (check.owner.getTeam() == team) { + + var manifold = gameServer.checkCellCollision(cell, check); // Calculation info + if (manifold != null) { // Collided + // Call gameserver's function to collide cells + gameServer.resolveCollision(manifold); + } + } + } +}; + +Teams.prototype.updateLB = function (gameServer) { + gameServer.leaderboardType = this.packetLB; + var total = 0; + var teamMass = []; + // Get mass + for (var i = 0; i < this.teamAmount; i++) { + // Set starting mass + teamMass[i] = 0; + + // Loop through cells + for (var j = 0; j < this.nodes[i].length; j++) { + var cell = this.nodes[i][j]; + + if (!cell) { + continue; + } + + teamMass[i] += cell.getMass(); + total += cell.getMass(); + } + } + // No players + if (total <= 0) { + for (var i = 0; i < this.teamAmount; i++) { + gameServer.leaderboard[i] = 0; + } + return + } + // Calc percentage + for (var i = 0; i < this.teamAmount; i++) { + gameServer.leaderboard[i] = teamMass[i] / total; + } +}; diff --git a/src/gamemodes/Tournament.js b/src/gamemodes/Tournament.js index c04f7d06e..8dd49c01c 100644 --- a/src/gamemodes/Tournament.js +++ b/src/gamemodes/Tournament.js @@ -1,248 +1,248 @@ -var Mode = require('./Mode'); - -function Tournament() { - Mode.apply(this, Array.prototype.slice.call(arguments)); - - this.ID = 10; - this.name = "Tournament"; - this.packetLB = 48; - - // Config (1 tick = 1000 ms) - this.prepTime = 5; // Amount of ticks after the server fills up to wait until starting the game - this.endTime = 15; // Amount of ticks after someone wins to restart the game - this.autoFill = false; - this.autoFillPlayers = 1; - this.dcTime = 0; - - // Gamemode Specific Variables - this.gamePhase = 0; // 0 = Waiting for players, 1 = Prepare to start, 2 = Game in progress, 3 = End - this.contenders = []; - this.maxContenders = 12; - - this.winner; - this.timer; - this.timeLimit = 3600; // in seconds -} - -module.exports = Tournament; -Tournament.prototype = new Mode(); - -// Gamemode Specific Functions - -Tournament.prototype.startGamePrep = function(gameServer) { - this.gamePhase = 1; - this.timer = this.prepTime; // 10 seconds -}; - -Tournament.prototype.startGame = function(gameServer) { - gameServer.run = true; - this.gamePhase = 2; - this.getSpectate(); // Gets a random person to spectate - gameServer.config.playerDisconnectTime = this.dcTime; // Reset config -}; - -Tournament.prototype.endGame = function(gameServer) { - this.winner = this.contenders[0]; - this.gamePhase = 3; - this.timer = this.endTime; // 30 Seconds -}; - -Tournament.prototype.endGameTimeout = function(gameServer) { - gameServer.run = false; - this.gamePhase = 4; - this.timer = this.endTime; // 30 Seconds -}; - -Tournament.prototype.fillBots = function(gameServer) { - // Fills the server with bots if there arent enough players - var fill = this.maxContenders - this.contenders.length; - for (var i = 0; i < fill; i++) { - gameServer.bots.addBot(); - } -}; - -Tournament.prototype.getSpectate = function() { - // Finds a random person to spectate - var index = Math.floor(Math.random() * this.contenders.length); - this.rankOne = this.contenders[index]; -}; - -Tournament.prototype.prepare = function(gameServer) { - // Remove all cells - var len = gameServer.nodes.length; - for (var i = 0; i < len; i++) { - var node = gameServer.nodes[0]; - - if (!node) { - continue; - } - - gameServer.removeNode(node); - } - - gameServer.bots.loadNames(); - - // Pauses the server - gameServer.run = false; - this.gamePhase = 0; - - // Get config values - if (gameServer.config.tourneyAutoFill > 0) { - this.timer = gameServer.config.tourneyAutoFill; - this.autoFill = true; - this.autoFillPlayers = gameServer.config.tourneyAutoFillPlayers; - } - // Handles disconnections - this.dcTime = gameServer.config.playerDisconnectTime; - gameServer.config.playerDisconnectTime = 0; - gameServer.config.playerMinMassDecay = gameServer.config.playerStartMass; - - this.prepTime = gameServer.config.tourneyPrepTime; - this.endTime = gameServer.config.tourneyEndTime; - this.maxContenders = gameServer.config.tourneyMaxPlayers; - - // Time limit - this.timeLimit = gameServer.config.tourneyTimeLimit * 60; // in seconds -}; - -Tournament.prototype.onPlayerDeath = function(gameServer) { - // Nothing -}; - -Tournament.prototype.formatTime = function(time) { - if (time < 0) { - return "0:00"; - } - // Format - var min = Math.floor(this.timeLimit / 60); - var sec = this.timeLimit % 60; - sec = (sec > 9) ? sec : "0" + sec.toString(); - return min + ":" + sec; -}; - -// Override - -Tournament.prototype.onServerInit = function(gameServer) { - this.prepare(gameServer); -}; - -Tournament.prototype.onPlayerSpawn = function(gameServer, player) { - // Only spawn players if the game hasnt started yet - if ((this.gamePhase == 0) && (this.contenders.length < this.maxContenders)) { - player.color = gameServer.getRandomColor(); // Random color - this.contenders.push(player); // Add to contenders list - gameServer.spawnPlayer(player); - - if (this.contenders.length == this.maxContenders) { - // Start the game once there is enough players - this.startGamePrep(gameServer); - } - } -}; - -Tournament.prototype.onCellRemove = function(cell) { - var owner = cell.owner, - human_just_died = false; - - if (owner.cells.length <= 0) { - // Remove from contenders list - var index = this.contenders.indexOf(owner); - if (index != -1) { - if ('_socket' in this.contenders[index].socket) { - human_just_died = true; - } - this.contenders.splice(index, 1); - } - - // Victory conditions - var humans = 0; - for (var i = 0; i < this.contenders.length; i++) { - if ('_socket' in this.contenders[i].socket) { - humans++; - } - } - - // the game is over if: - // 1) there is only 1 player left, OR - // 2) all the humans are dead, OR - // 3) the last-but-one human just died - if ((this.contenders.length == 1 || humans == 0 || (humans == 1 && human_just_died)) && this.gamePhase == 2) { - this.endGame(cell.owner.gameServer); - } else { - // Do stuff - this.onPlayerDeath(cell.owner.gameServer); - } - } -}; - -Tournament.prototype.updateLB = function(gameServer) { - var lb = gameServer.leaderboard; - - switch (this.gamePhase) { - case 0: - lb[0] = "Waiting for"; - lb[1] = "players: "; - lb[2] = this.contenders.length + "/" + this.maxContenders; - if (this.autoFill) { - if (this.timer <= 0) { - this.fillBots(gameServer); - } else if (this.contenders.length >= this.autoFillPlayers) { - this.timer--; - } - } - break; - case 1: - lb[0] = "Game starting in"; - lb[1] = this.timer.toString(); - lb[2] = "Good luck!"; - if (this.timer <= 0) { - // Reset the game - this.startGame(gameServer); - } else { - this.timer--; - } - break; - case 2: - lb[0] = "Players Remaining"; - lb[1] = this.contenders.length + "/" + this.maxContenders; - lb[2] = "Time Limit:"; - lb[3] = this.formatTime(this.timeLimit); - if (this.timeLimit < 0) { - // Timed out - this.endGameTimeout(gameServer); - } else { - this.timeLimit--; - } - break; - case 3: - lb[0] = "Congratulations"; - lb[1] = this.winner.getName(); - lb[2] = "for winning!"; - if (this.timer <= 0) { - // Reset the game - this.onServerInit(gameServer); - // Respawn starting food - gameServer.startingFood(); - } else { - lb[3] = "Game restarting in"; - lb[4] = this.timer.toString(); - this.timer--; - } - break; - case 4: - lb[0] = "Time Limit"; - lb[1] = "Reached!"; - if (this.timer <= 0) { - // Reset the game - this.onServerInit(gameServer); - // Respawn starting food - gameServer.startingFood(); - } else { - lb[2] = "Game restarting in"; - lb[3] = this.timer.toString(); - this.timer--; - } - default: - break; - } -}; +var Mode = require('./Mode'); + +function Tournament() { + Mode.apply(this, Array.prototype.slice.call(arguments)); + + this.ID = 10; + this.name = "Tournament"; + this.packetLB = 48; + + // Config (1 tick = 1000 ms) + this.prepTime = 5; // Amount of ticks after the server fills up to wait until starting the game + this.endTime = 15; // Amount of ticks after someone wins to restart the game + this.autoFill = false; + this.autoFillPlayers = 1; + this.dcTime = 0; + + // Gamemode Specific Variables + this.gamePhase = 0; // 0 = Waiting for players, 1 = Prepare to start, 2 = Game in progress, 3 = End + this.contenders = []; + this.maxContenders = 12; + + this.winner; + this.timer; + this.timeLimit = 3600; // in seconds +} + +module.exports = Tournament; +Tournament.prototype = new Mode(); + +// Gamemode Specific Functions + +Tournament.prototype.startGamePrep = function (gameServer) { + this.gamePhase = 1; + this.timer = this.prepTime; // 10 seconds +}; + +Tournament.prototype.startGame = function (gameServer) { + gameServer.run = true; + this.gamePhase = 2; + this.getSpectate(); // Gets a random person to spectate + gameServer.config.playerDisconnectTime = this.dcTime; // Reset config +}; + +Tournament.prototype.endGame = function (gameServer) { + this.winner = this.contenders[0]; + this.gamePhase = 3; + this.timer = this.endTime; // 30 Seconds +}; + +Tournament.prototype.endGameTimeout = function (gameServer) { + gameServer.run = false; + this.gamePhase = 4; + this.timer = this.endTime; // 30 Seconds +}; + +Tournament.prototype.fillBots = function (gameServer) { + // Fills the server with bots if there arent enough players + var fill = this.maxContenders - this.contenders.length; + for (var i = 0; i < fill; i++) { + gameServer.bots.addBot(); + } +}; + +Tournament.prototype.getSpectate = function () { + // Finds a random person to spectate + var index = Math.floor(Math.random() * this.contenders.length); + this.rankOne = this.contenders[index]; +}; + +Tournament.prototype.prepare = function (gameServer) { + // Remove all cells + var len = gameServer.nodes.length; + for (var i = 0; i < len; i++) { + var node = gameServer.nodes[0]; + + if (!node) { + continue; + } + + gameServer.removeNode(node); + } + + gameServer.bots.loadNames(); + + // Pauses the server + gameServer.run = false; + this.gamePhase = 0; + + // Get config values + if (gameServer.config.tourneyAutoFill > 0) { + this.timer = gameServer.config.tourneyAutoFill; + this.autoFill = true; + this.autoFillPlayers = gameServer.config.tourneyAutoFillPlayers; + } + // Handles disconnections + this.dcTime = gameServer.config.playerDisconnectTime; + gameServer.config.playerDisconnectTime = 0; + + this.prepTime = gameServer.config.tourneyPrepTime; + this.endTime = gameServer.config.tourneyEndTime; + this.maxContenders = gameServer.config.tourneyMaxPlayers; + + // Time limit + this.timeLimit = gameServer.config.tourneyTimeLimit * 60; // in seconds +}; + +Tournament.prototype.onPlayerDeath = function (gameServer) { + // Nothing +}; + +Tournament.prototype.formatTime = function (time) { + if (time < 0) { + return "0:00"; + } + // Format + var min = Math.floor(this.timeLimit / 60); + var sec = this.timeLimit % 60; + sec = (sec > 9) ? sec : "0" + sec.toString(); + return min + ":" + sec; +}; + +// Override + +Tournament.prototype.onServerInit = function (gameServer) { + this.prepare(gameServer); +}; + +Tournament.prototype.onPlayerSpawn = function (gameServer, player) { + // Only spawn players if the game hasnt started yet + if ((this.gamePhase == 0) && (this.contenders.length < this.maxContenders)) { + player.setColor(gameServer.getRandomColor()); // Random color + this.contenders.push(player); // Add to contenders list + gameServer.spawnPlayer(player); + + if (this.contenders.length == this.maxContenders) { + // Start the game once there is enough players + this.startGamePrep(gameServer); + } + } +}; + +Tournament.prototype.onCellRemove = function (cell) { + var owner = cell.owner, + human_just_died = false; + + if (owner.cells.length <= 0) { + // Remove from contenders list + var index = this.contenders.indexOf(owner); + if (index != -1) { + if ('_socket' in this.contenders[index].socket) { + human_just_died = true; + } + this.contenders.splice(index, 1); + } + + // Victory conditions + var humans = 0; + for (var i = 0; i < this.contenders.length; i++) { + if ('_socket' in this.contenders[i].socket) { + humans++; + } + } + + // the game is over if: + // 1) there is only 1 player left, OR + // 2) all the humans are dead, OR + // 3) the last-but-one human just died + if ((this.contenders.length == 1 || humans == 0 || (humans == 1 && human_just_died)) && this.gamePhase == 2) { + this.endGame(cell.owner.gameServer); + } else { + // Do stuff + this.onPlayerDeath(cell.owner.gameServer); + } + } +}; + +Tournament.prototype.updateLB = function (gameServer) { + gameServer.leaderboardType = this.packetLB; + var lb = gameServer.leaderboard; + + switch (this.gamePhase) { + case 0: + lb[0] = "Waiting for"; + lb[1] = "players: "; + lb[2] = this.contenders.length + "/" + this.maxContenders; + if (this.autoFill) { + if (this.timer <= 0) { + this.fillBots(gameServer); + } else if (this.contenders.length >= this.autoFillPlayers) { + this.timer--; + } + } + break; + case 1: + lb[0] = "Game starting in"; + lb[1] = this.timer.toString(); + lb[2] = "Good luck!"; + if (this.timer <= 0) { + // Reset the game + this.startGame(gameServer); + } else { + this.timer--; + } + break; + case 2: + lb[0] = "Players Remaining"; + lb[1] = this.contenders.length + "/" + this.maxContenders; + lb[2] = "Time Limit:"; + lb[3] = this.formatTime(this.timeLimit); + if (this.timeLimit < 0) { + // Timed out + this.endGameTimeout(gameServer); + } else { + this.timeLimit--; + } + break; + case 3: + lb[0] = "Congratulations"; + lb[1] = this.winner.getName(); + lb[2] = "for winning!"; + if (this.timer <= 0) { + // Reset the game + this.onServerInit(gameServer); + // Respawn starting food + gameServer.startingFood(); + } else { + lb[3] = "Game restarting in"; + lb[4] = this.timer.toString(); + this.timer--; + } + break; + case 4: + lb[0] = "Time Limit"; + lb[1] = "Reached!"; + if (this.timer <= 0) { + // Reset the game + this.onServerInit(gameServer); + // Respawn starting food + gameServer.startingFood(); + } else { + lb[2] = "Game restarting in"; + lb[3] = this.timer.toString(); + this.timer--; + } + default: + break; + } +}; diff --git a/src/gamemodes/Zombie.js b/src/gamemodes/Zombie.js index 59d8f9ea3..bb6a30dbd 100755 --- a/src/gamemodes/Zombie.js +++ b/src/gamemodes/Zombie.js @@ -1,177 +1,181 @@ -var Mode = require('./Mode'); - -function Zombie() { - Mode.apply(this, Array.prototype.slice.call(arguments)); - - this.ID = 12; - this.name = "Zombie FFA"; - this.haveTeams = true; - this.zombieColor = { - 'r': 223, - 'g': 223, - 'b': 223 - }; - this.zombies = []; - this.players = []; -} - -module.exports = Zombie; -Zombie.prototype = new Mode(); - -// Gamemode Specific Functions - -Zombie.prototype.leaderboardAddSort = function(player, leaderboard) { - // Adds the player and sorts the leaderboard - var len = leaderboard.length - 1; - var loop = true; - while ((len >= 0) && (loop)) { - // Start from the bottom of the leaderboard - if (player.getScore(false) <= leaderboard[len].getScore(false)) { - leaderboard.splice(len + 1, 0, player); - loop = false; // End the loop if a spot is found - } - len--; - } - if (loop) { - // Add to top of the list because no spots were found - leaderboard.splice(0, 0, player); - } -}; - -Zombie.prototype.makeZombie = function(player) { - // turns a player into a zombie - player.team = 0; - player.color = this.zombieColor; - for (var i = 0; i < player.cells.length; i++) { - // remove cell from players array - var index = this.players.indexOf(player.cells[i]); - if (index != -1) { - this.players.splice(index, 1); - } - // change color of cell - player.cells[i].color = this.zombieColor; - // add cell to zombie array - this.zombies.push(player.cells[i]); - } -}; - -// Override - -Zombie.prototype.onPlayerSpawn = function(gameServer, player) { - // make player a zombie if there are none - if (this.zombies.length == 0) { - player.team = 0; - player.color = this.zombieColor; - } else { - // use player id as team so that bots are still able to fight (even though they probably turn into zombies very fast) - player.team = player.pID; - player.color = gameServer.getRandomColor(); - } - - // Spawn player - gameServer.spawnPlayer(player); -}; - -Zombie.prototype.onCellAdd = function(cell) { - // Add to team list - if (cell.owner.getTeam() == 0) { - this.zombies.push(cell); - } else { - this.players.push(cell); - } -}; - -Zombie.prototype.onCellRemove = function(cell) { - // Remove from team list - if (cell.owner.getTeam() == 0) { - var index = this.zombies.indexOf(cell); - if (index != -1) { - this.zombies.splice(index, 1); - } - } else { - var index = this.players.indexOf(cell); - if (index != -1) { - this.players.splice(index, 1); - } - } -}; - -Zombie.prototype.onCellMove = function(x1, y1, cell) { - var team = cell.owner.getTeam(); - var r = cell.getSize(); - - // Find team - for (var i = 0; i < cell.owner.visibleNodes.length; i++) { - // Only collide with player cells - var check = cell.owner.visibleNodes[i]; - - if ((check.getType() != 0) || (cell.owner == check.owner)) { - continue; - } - - // Collision with zombies - if (check.owner.getTeam() == team || check.owner.getTeam() == 0 || team == 0) { - // Check if in collision range - var collisionDist = check.getSize() + r; // Minimum distance between the 2 cells - if (!cell.simpleCollide(x1, y1, check, collisionDist)) { - // Skip - continue; - } - - // First collision check passed... now more precise checking - dist = cell.getDist(cell.position.x, cell.position.y, check.position.x, check.position.y); - - // Calculations - if (dist < collisionDist) { // Collided - if (check.owner.getTeam() == 0 && team != 0) { - // turn player into zombie - this.makeZombie(cell.owner); - } else if (team == 0 && check.owner.getTeam() != 0) { - // turn other player into zombie - this.makeZombie(check.owner); - } - // The moving cell pushes the colliding cell - var newDeltaY = check.position.y - y1; - var newDeltaX = check.position.x - x1; - var newAngle = Math.atan2(newDeltaX, newDeltaY); - - var move = collisionDist - dist; - - check.position.x = check.position.x + (move * Math.sin(newAngle)) >> 0; - check.position.y = check.position.y + (move * Math.cos(newAngle)) >> 0; - } - } - } -}; - -Zombie.prototype.updateLB = function(gameServer) { - var lb = gameServer.leaderboard; - // Loop through all clients - for (var i = 0; i < gameServer.clients.length; i++) { - if (typeof gameServer.clients[i] == "undefined" || gameServer.clients[i].playerTracker.team == 0) { - continue; - } - - var player = gameServer.clients[i].playerTracker; - var playerScore = player.getScore(true); - if (player.cells.length <= 0) { - continue; - } - - if (lb.length == 0) { - // Initial player - lb.push(player); - continue; - } else if (lb.length < 10) { - this.leaderboardAddSort(player, lb); - } else { - // 10 in leaderboard already - if (playerScore > lb[9].getScore(false)) { - lb.pop(); - this.leaderboardAddSort(player, lb); - } - } - } - - this.rankOne = lb[0]; -}; +var Mode = require('./Mode'); + +function Zombie() { + Mode.apply(this, Array.prototype.slice.call(arguments)); + + this.ID = 12; + this.name = "Zombie FFA"; + this.haveTeams = true; + this.zombieColor = { + 'r': 223, + 'g': 223, + 'b': 223 + }; + this.zombies = []; + this.players = []; +} + +module.exports = Zombie; +Zombie.prototype = new Mode(); + +// Gamemode Specific Functions + +Zombie.prototype.leaderboardAddSort = function (player, leaderboard) { + // Adds the player and sorts the leaderboard + var len = leaderboard.length - 1; + var loop = true; + while ((len >= 0) && (loop)) { + // Start from the bottom of the leaderboard + if (player.getScore() <= leaderboard[len].getScore()) { + leaderboard.splice(len + 1, 0, player); + loop = false; // End the loop if a spot is found + } + len--; + } + if (loop) { + // Add to top of the list because no spots were found + leaderboard.splice(0, 0, player); + } +}; + +Zombie.prototype.makeZombie = function (player) { + // turns a player into a zombie + player.team = 0; + player.setColor(this.zombieColor); + for (var i = 0; i < player.cells.length; i++) { + // remove cell from players array + var index = this.players.indexOf(player.cells[i]); + if (index != -1) { + this.players.splice(index, 1); + } + // change color of cell + player.cells[i].setColor(this.zombieColor); + // add cell to zombie array + this.zombies.push(player.cells[i]); + } +}; + +// Override + +Zombie.prototype.onPlayerSpawn = function (gameServer, player) { + // make player a zombie if there are none + if (this.zombies.length == 0) { + player.team = 0; + player.setColor(this.zombieColor); + } else { + // use player id as team so that bots are still able to fight (even though they probably turn into zombies very fast) + player.team = player.pID; + player.setColor(gameServer.getRandomColor()); + } + + // Spawn player + gameServer.spawnPlayer(player); +}; + +Zombie.prototype.onCellAdd = function (cell) { + // Add to team list + if (cell.owner.getTeam() == 0) { + this.zombies.push(cell); + } else { + this.players.push(cell); + } +}; + +Zombie.prototype.onCellRemove = function (cell) { + // Remove from team list + if (cell.owner.getTeam() == 0) { + var index = this.zombies.indexOf(cell); + if (index != -1) { + this.zombies.splice(index, 1); + } + } else { + var index = this.players.indexOf(cell); + if (index != -1) { + this.players.splice(index, 1); + } + } +}; + +// TODO: remove it (move physics is managed by GameServer) +Zombie.prototype.onCellMove = function (x1, y1, cell) { + var team = cell.owner.getTeam(); + var r = cell.getSize(); + + // Find team + for (var i = 0; i < cell.owner.visibleNodes.length; i++) { + // Only collide with player cells + var check = cell.owner.visibleNodes[i]; + + if ((check.getType() != 0) || (cell.owner == check.owner)) { + continue; + } + + // Collision with zombies + if (check.owner.getTeam() == team || check.owner.getTeam() == 0 || team == 0) { + // Check if in collision range + var collisionDist = check.getSize() + r; // Minimum distance between the 2 cells + if (!cell.simpleCollide(x1, y1, check, collisionDist)) { + // Skip + continue; + } + + // First collision check passed... now more precise checking + dist = cell.getDist(cell.position.x, cell.position.y, check.position.x, check.position.y); + + // Calculations + if (dist < collisionDist) { // Collided + if (check.owner.getTeam() == 0 && team != 0) { + // turn player into zombie + this.makeZombie(cell.owner); + } else if (team == 0 && check.owner.getTeam() != 0) { + // turn other player into zombie + this.makeZombie(check.owner); + } + // The moving cell pushes the colliding cell + var newDeltaY = check.position.y - y1; + var newDeltaX = check.position.x - x1; + var newAngle = Math.atan2(newDeltaX, newDeltaY); + + var move = collisionDist - dist; + + check.setPosition({ + x: check.position.x + (move * Math.sin(newAngle)) >> 0, + y: check.position.y + (move * Math.cos(newAngle)) >> 0 + }); + } + } + } +}; + +Zombie.prototype.updateLB = function (gameServer) { + gameServer.leaderboardType = this.packetLB; + var lb = gameServer.leaderboard; + // Loop through all clients + for (var i = 0; i < gameServer.clients.length; i++) { + if (typeof gameServer.clients[i] == "undefined" || gameServer.clients[i].playerTracker.team == 0) { + continue; + } + + var player = gameServer.clients[i].playerTracker; + var playerScore = player.getScore(); + if (player.cells.length <= 0) { + continue; + } + + if (lb.length == 0) { + // Initial player + lb.push(player); + continue; + } else if (lb.length < 10) { + this.leaderboardAddSort(player, lb); + } else { + // 10 in leaderboard already + if (playerScore > lb[9].getScore()) { + lb.pop(); + this.leaderboardAddSort(player, lb); + } + } + } + + this.rankOne = lb[0]; +}; diff --git a/src/gamemodes/index.js b/src/gamemodes/index.js index 11d89e0d0..25df2bdc3 100755 --- a/src/gamemodes/index.js +++ b/src/gamemodes/index.js @@ -1,4 +1,4 @@ -module.exports = { +module.exports = { Mode: require('./Mode'), FFA: require('./FFA'), Teams: require('./Teams'), @@ -6,43 +6,39 @@ module.exports = { Tournament: require('./Tournament'), HungerGames: require('./HungerGames'), Rainbow: require('./Rainbow'), - Debug: require('./Debug'), Zombie: require('./Zombie'), TeamZ: require('./TeamZ.js'), TeamX: require('./TeamX.js') }; -var get = function(id) { +var get = function (id) { var mode; switch (id) { - case 1: // Teams + case 1:// Teams mode = new module.exports.Teams(); break; - case 2: // Experimental + case 2:// Experimental mode = new module.exports.Experimental(); break; - case 10: // Tournament + case 10:// Tournament mode = new module.exports.Tournament(); break; - case 11: // Hunger Games + case 11:// Hunger Games mode = new module.exports.HungerGames(); break; - case 12: // Zombie + case 12:// Zombie mode = new module.exports.Zombie(); break; - case 13: // Zombie Team + case 13:// Zombie Team mode = new module.exports.TeamZ(); break; - case 14: // Experimental Team + case 14:// Experimental Team mode = new module.exports.TeamX(); break; - case 20: // Rainbow + case 20:// Rainbow mode = new module.exports.Rainbow(); break; - case 21: // Debug - mode = new module.exports.Debug(); - break; - default: // FFA is default + default:// FFA is default mode = new module.exports.FFA(); break; } diff --git a/src/gameserver.ini b/src/gameserver.ini index f8613d15b..83b25554b 100644 --- a/src/gameserver.ini +++ b/src/gameserver.ini @@ -1,95 +1,141 @@ -// Ogar configurations file -// Lines starting with slashes are comment lines - -// [Server] -// serverGamemode: 0 = FFA, 1 = Teams, 2 = Experimental, 10 = Tournament, 11 = Hunger Games -// serverBots: Amount of player bots to spawn (Experimental) -// serverViewBase: Base view distance of players. Warning: high values may cause lag -// serverStatsPort: Port for the stats server. Having a negative number will disable the stats server. -// serverStatsUpdate: Amount of seconds per update for server stats -// serverLogLevel: Logging level of the server. 0 = No logs, 1 = Logs the console, 2 = Logs console and ip connections -// serverScrambleCoords: Toggles scrambling of coordinates. 0 = No scrambling, 1 = scrambling. Default is 1. -// serverScrambleMinimaps: Toggles scrambling of borders to render maps unusable. 0 = No scrambling, 1 = scrambling. Default is 1. -// serverTeamingAllowed: Toggles anti-teaming. 0 = Anti-team enabled, 1 = Anti-team disabled. Default is 1. -// serverMaxLB: Controls the maximum players displayed on the leaderboard. -serverMaxConnections = 64 -serverPort = 443 -serverGamemode = 0 -serverBots = 0 -serverViewBaseX = 1024 -serverViewBaseY = 592 -serverStatsPort = 88 -serverStatsUpdate = 60 -serverLogLevel = 1 -serverScrambleCoords = 1 -serverScrambleMinimaps = 1 -serverTeamingAllowed = 1 -serverMaxLB = 10 - -// [Border] -// Border values of the map (Vanilla values are left/top = 0, right/bottom = 14142.135623730952) -borderLeft = 0 -borderRight = 6000 -borderTop = 0 -borderBottom = 6000 - -// [Spawn] -// Each interval is 1 tick (50 ms) -spawnInterval = 20 -foodSpawnAmount = 10 -foodStartAmount = 100 -foodMaxAmount = 500 -foodMass = 1 -foodMassGrow = 1 -foodMassGrowPossiblity = 50 -foodMassLimit = 5 -foodMassTimeout = 120 -virusMinAmount = 10 -virusMaxAmount = 50 -virusStartMass = 100 -virusFeedAmount = 7 - -// [Ejected Mass] -// ejectMass: Mass of ejected cells -// ejectMassCooldown: Time until a player can eject mass again (ms) -// ejectMassLoss: Mass lost when ejecting cells -// ejectSpeed: Base speed of ejected cells -// ejectSpawnPlayer: Chance for a player to spawn from ejected mass -ejectMass = 13 -ejectMassCooldown = 100 -ejectMassLoss = 15 -ejectSpeed = 100 -ejectSpawnPlayer = 50 - -// [Player] -// playerRecombineTime: Base amount of ticks before a cell is allowed to recombine (1 tick = 1000 milliseconds) -// playerBotGrowEnabled: If 0, eating a cell with less than 17 mass while cell has over 625 wont gain any mass -// playerMassAbsorbed: Fraction of a player cell that gets absorbed upon eating (i.e., 1 = 100%, 0.8 = 80%, etc) -// playerMassDecayRate: Amount of mass lost per tick (Multiplier) (1 tick = 1000 milliseconds) -// playerMinMassDecay: Minimum mass for decay to occur -// playerDisconnectTime: The amount of seconds it takes for a player cell to be removed after disconnection (If set to -1, cells are never removed) -playerStartMass = 10 -playerBotGrowEnabled = 1 -playerMaxMass = 22500 -playerMinMassEject = 32 -playerMinMassSplit = 36 -playerMaxCells = 16 -playerRecombineTime = 30 -playerMassAbsorbed = 1.0 -playerMassDecayRate = .002 -playerMinMassDecay = 9 -playerMaxNickLength = 15 -playerSpeed = 35 -playerDisconnectTime = 60 - -// [Gamemode] -// Custom gamemode settings -// tourneyTimeLimit: Time limit of the game, in minutes. -// tourneyAutoFill: If set to a value higher than 0, the tournament match will automatically fill up with bots after value seconds -// tourneyAutoFillPlayers: The timer for filling the server with bots will not count down unless there is this amount of real players -tourneyMaxPlayers = 12 -tourneyPrepTime = 10 -tourneyEndTime = 30 -tourneyTimeLimit = 20 -tourneyAutoFill = 0 -tourneyAutoFillPlayers = 1 +# MultiOgar configurations file +# Lines starting with number sign (#) are comments + +# [NOTE] +# MultiOgar uses cell size instead of cell mass to improve performance! +# In order to get the cell size from mass value, you need to calculate using this formula: +# size = SQRT( mass * 100 ) +# +# For example, to set start mass = 43: +# size = SQRT( 43 * 100 ) = SQRT( 4300 ) = 65.57 +# Set playerStartSize = 66 +# +# Also, you can use the following syntax to specify mass: +# playerStartSize = massToSize(43) +# It will be automatically converted to 66 + +# [Log] +# logVerbosity: Console log level (0=NONE; 1=FATAL; 2=ERROR; 3=WARN; 4=INFO; 5=DEBUG) +# logFileVerbosity: File log level +logVerbosity = 4 +logFileVerbosity = 5 + +# [Server] +# serverTimeout: Seconds to keep connection alive for non-responding client +# serverWsModule: WebSocket module: 'ws' or 'uws' (install npm package before using uws) +# serverIpLimit: Controls the maximum connections from single IP (use 0 to disable) +# serverMinionIgnoreTime: minion detection disable time on server startup [seconds] +# serverMinionThreshold: max connections within serverMinionInterval time period, which will not be marked as minion +# serverMinionInterval: minion detection interval [milliseconds] +# serverPort: Server port which will be used to listen for incoming connections +# serverBind: Server network interface which will be used to listen for incoming connections (0.0.0.0 for all IPv4 interfaces) +# serverTracker: Set to 1 if you want to show your server on the tracker http://ogar.mivabe.nl/master (check that your server port is opened for external connections before setting it to 1) +# serverGamemode: 0 = FFA, 1 = Teams, 2 = Experimental, 10 = Tournament, 11 = Hunger Games +# serverBots: Number of player bots to spawn (Experimental) +# serverViewBase: Base view distance of players. Warning: high values may cause lag! Min value is 1920x1080 +# serverMinScale: Min scale for player (low value leads to lags due to large visible area for big cell) +# serverSpectatorScale: Scale (field of view) used for free roam spectators (low value leads to lags, vanilla=0.4, old vanilla=0.25) +# serverStatsPort: Port for the stats server. Having a negative number will disable the stats server. +# serverStatsUpdate: Update interval of server stats in seconds +# serverScrambleLevel: Toggles scrambling of coordinates. 0 = No scrambling, 1 = lightweight scrambling. 2 = full scrambling (also known as scramble minimap), 3 - high level scrambling (no border) +# serverMaxLB: Controls the maximum players displayed on the leaderboard. +# serverChat: Allows the usage of server chat. 0 = no chat, 1 = use chat. +# serverName: Server name, for example "My great server" +# serverWelcome1: First server welcome message +# serverWelcome2: Second server welcome message (optional, for info, etc) +serverTimeout = 300 +serverWsModule = "ws" +serverMaxConnections = 128 +serverIpLimit = 4 +serverMinionIgnoreTime = 30 +serverMinionThreshold = 10 +serverMinionInterval = 1000 +serverPort = 443 +serverBind = "0.0.0.0" +serverTracker = 0 +serverGamemode = 0 +serverBots = 0 +serverViewBaseX = 1920 +serverViewBaseY = 1080 +serverMinScale = 0.15 +serverSpectatorScale = 0.4 +serverStatsPort = 88 +serverStatsUpdate = 60 +serverScrambleLevel = 2 +serverMaxLB = 10 +serverChat = 1 +serverChatAscii = 1 +serverName = "MultiOgar #1" +serverWelcome1 = "Welcome to MultiOgar server!" +serverWelcome2 = "" + +# [Border] +# Border size (vanilla 14142.135623730952) +borderWidth = 14142 +borderHeight = 14142 + +# [Spawn] +# Each interval is 1 tick (40 ms) +# foodMinSize: vanilla 10 (mass = 10*10/100 = 1) +# foodMaxSize: vanilla 20 (mass = 20*20/100 = 4) +foodMinSize = 10 +foodMaxSize = 20 +foodMinAmount = 1000 +foodMaxAmount = 2000 +foodSpawnAmount = 30 +foodMassGrow = 1 +spawnInterval = 20 + +# virusMinSize: vanilla 100 (mass = 100*100/100 = 100) +# virusMaxSize: vanilla 140 (mass = 140*140/100 = 196) +virusMinSize = 100 +virusMaxSize = 140 +virusMinAmount = 50 +virusMaxAmount = 100 + +# [Ejected Mass] +# ejectSize: vanilla 37 (mass = 37*37/100 = 13.69) +# ejectSizeLoss: Eject size which will be substracted from player cell (vanilla 43?) +# ejectDistance: vanilla 780 +# ejectCooldown: Tick count until a player can eject mass again (1 tick = 40 ms) +# ejectSpawnPlayer: if 1 then player may be spawned from ejected mass +ejectSize = 38 +ejectSizeLoss = 43 +ejectDistance = 780 +ejectCooldown = 3 +ejectSpawnPlayer = 1 + +# [Player] +# Reminder: MultiOgar uses cell size instead of mass! +# playerStartMass replaced with playerStartSize +# +# playerMinSize: vanilla 32 (mass = 32*32/100 = 10.24) +# playerMaxSize: vanilla 1500 (mass = 1500*1500/100 = 22500) +# playerMinSplitSize: vanilla 60 (mass = 60*60/100 = 36) +# playerStartSize: Start size of the player cell (mass = 64*64/100 = 41) +# playerSpeed: Player speed multiplier (1=normal speed, 2=twice faster) +# playerRecombineTime: Base time in seconds before a cell is allowed to recombine +# playerDecayRate: Amount of size lost per second +# playerDisconnectTime: Time in seconds before a disconnected player's cell is removed (Set to -1 to never remove) +playerMinSize = 32 +playerMaxSize = 1500 +playerMinSplitSize = 60 +playerStartSize = 64 +playerMaxCells = 16 +playerSpeed = 1 +playerDecayRate = .002 +playerRecombineTime = 30 +playerMaxNickLength = 15 +playerDisconnectTime = 60 + +# [Gamemode] +# Custom gamemode settings +# tourneyTimeLimit: Time limit of the game, in minutes. +# tourneyAutoFill: If set to a value higher than 0, the tournament match will automatically fill up with bots after value seconds +# tourneyAutoFillPlayers: The timer for filling the server with bots will not count down unless there is this amount of real players +tourneyMaxPlayers = 12 +tourneyPrepTime = 10 +tourneyEndTime = 30 +tourneyTimeLimit = 20 +tourneyAutoFill = 0 +tourneyAutoFillPlayers = 1 diff --git a/src/index.js b/src/index.js index b12016ac3..d5febef5d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,68 +1,88 @@ -// Imports -var Commands = require('./modules/CommandList'); -var GameServer = require('./GameServer'); - -// Init variables -var showConsole = true; - -// Start msg -console.log("[Game] Ogar - An open source Agar.io server implementation"); - -// Handle arguments -process.argv.forEach(function(val) { - if (val == "--noconsole") { - showConsole = false; - } else if (val == "--help") { - console.log("Proper Usage: node index.js"); - console.log(" --noconsole Disables the console"); - console.log(" --help Help menu."); - console.log(""); - } -}); - -// Run Ogar -var gameServer = new GameServer(); -gameServer.start(); -// Add command handler -gameServer.commands = Commands.list; -// Initialize the server console -if (showConsole) { - var readline = require('readline'); - var in_ = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - setTimeout(prompt, 100); -} - -// Console functions - -function prompt() { - in_.question(">", function(str) { - parseCommands(str); - return prompt(); // Too lazy to learn async - }); -} - -function parseCommands(str) { - // Log the string - gameServer.log.onCommand(str); - - // Don't process ENTER - if (str === '') - return; - - // Splits the string - var split = str.split(" "); - - // Process the first string value - var first = split[0].toLowerCase(); - - // Get command function - var execute = gameServer.commands[first]; - if (typeof execute != 'undefined') { - execute(gameServer, split); - } else { - console.log("[Console] Invalid Command!"); - } -} +// Imports +var pjson = require('../package.json'); +var Logger = require('./modules/Logger'); +var Commands = require('./modules/CommandList'); +var GameServer = require('./GameServer'); + +// Init variables +var showConsole = true; + +// Start msg +Logger.start(); + +process.on('exit', function (code) { + Logger.debug("process.exit(" + code + ")"); + Logger.shutdown(); +}); + +process.on('uncaughtException', function (err) { + Logger.fatal(err.stack); + process.exit(1); +}); + +Logger.info("\u001B[1m\u001B[32mMultiOgar " + pjson.version + "\u001B[37m - An open source multi-protocol ogar server\u001B[0m"); + + +// Handle arguments +process.argv.forEach(function (val) { + if (val == "--noconsole") { + showConsole = false; + } else if (val == "--help") { + console.log("Proper Usage: node index.js"); + console.log(" --noconsole Disables the console"); + console.log(" --help Help menu."); + console.log(""); + } +}); + +// Run Ogar +var gameServer = new GameServer(); +gameServer.start(); +// Add command handler +gameServer.commands = Commands.list; +// Initialize the server console +if (showConsole) { + var readline = require('readline'); + var in_ = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + setTimeout(prompt, 100); +} + +// Console functions + +function prompt() { + in_.question(">", function (str) { + try { + parseCommands(str); + } catch (err) { + Logger.error(err.stack); + } finally { + setTimeout(prompt, 0); + } + }); +} + +function parseCommands(str) { + // Log the string + Logger.write(">" + str); + + // Don't process ENTER + if (str === '') + return; + + // Splits the string + var split = str.split(" "); + + // Process the first string value + var first = split[0].toLowerCase(); + + // Get command function + var execute = gameServer.commands[first]; + if (typeof execute != 'undefined') { + execute(gameServer, split); + } else { + Logger.warn("Invalid Command!"); + } +} diff --git a/src/ipbanlist.txt b/src/ipbanlist.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/modules/CommandList.js b/src/modules/CommandList.js index 41fbc3ede..265887edb 100644 --- a/src/modules/CommandList.js +++ b/src/modules/CommandList.js @@ -1,539 +1,713 @@ -// Imports -var GameMode = require('../gamemodes'); -var Entity = require('../entity'); - -function Commands() { - this.list = {}; // Empty -} - -module.exports = Commands; - -// Utils -var fillChar = function(data, char, fieldLength, rTL) { - var result = data.toString(); - if (rTL === true) { - for (var i = result.length; i < fieldLength; i++) - result = char.concat(result); - } else { - for (var i = result.length; i < fieldLength; i++) - result = result.concat(char); - } - return result; -}; - -// Commands - -Commands.list = { - help: function(gameServer, split) { - console.log("[Console] ======================== HELP ======================"); - console.log("[Console] addbot [number] : add bot to the server"); - console.log("[Console] kickbot [number] : kick an amount of bots"); - console.log("[Console] board [string] [string] ... : set scoreboard text"); - console.log("[Console] boardreset : reset scoreboard text"); - console.log("[Console] change [setting] [value] : change specified settings"); - console.log("[Console] clear : clear console output"); - console.log("[Console] color [PlayerID] [R] [G] [B] : set cell(s) color by client ID"); - console.log("[Console] exit : stop the server"); - console.log("[Console] food [X] [Y] [mass] : spawn food at specified Location"); - console.log("[Console] gamemode [id] : change server gamemode"); - console.log("[Console] kick [PlayerID] : kick player or bot by client ID"); - console.log("[Console] kickall : kick all players and bots"); - console.log("[Console] kill [PlayerID] : kill cell(s) by client ID"); - console.log("[Console] killall : kill everyone"); - console.log("[Console] mass [PlayerID] [mass] : set cell(s) mass by client ID"); - console.log("[Console] merge [PlayerID] : merge all client's cells once"); - console.log("[Console] name [PlayerID] [name] : change cell(s) name by client ID"); - console.log("[Console] playerlist : get list of players and bots"); - console.log("[Console] pause : pause game , freeze all cells"); - console.log("[Console] reload : reload config"); - console.log("[Console] resetantiteam [PlayerID] : reset anti-team effect on client"); - console.log("[Console] status : get server status"); - console.log("[Console] tp [PlayerID] [X] [Y] : teleport player to specified location"); - console.log("[Console] virus [X] [Y] [mass] : spawn virus at a specified Location"); - console.log("[Console] pl : alias for playerlist"); - console.log("[Console] st : alias for status"); - console.log("[Console] ===================================================="); - }, - addbot: function(gameServer, split) { - var add = parseInt(split[1]); - if (isNaN(add)) { - add = 1; // Adds 1 bot if user doesnt specify a number - } - - for (var i = 0; i < add; i++) { - setTimeout(gameServer.bots.addBot.bind(gameServer.bots), i); - } - console.log("[Console] Added " + add + " player bots"); - }, - kickbot: function(gameServer, split) { - var toRemove = parseInt(split[1]); - if (isNaN(toRemove)) { - toRemove = -1; // Kick all bots if user doesnt specify a number - } - - var removed = 0; - var i = 0; - while (i < gameServer.clients.length && removed != toRemove) { - if (typeof gameServer.clients[i].remoteAddress == 'undefined') { // if client i is a bot kick him - var client = gameServer.clients[i].playerTracker; - var len = client.cells.length; - for (var j = 0; j < len; j++) { - gameServer.removeNode(client.cells[0]); - } - client.socket.close(); - removed++; - } else - i++; - } - if (toRemove == -1) - console.log("[Console] Kicked all bots (" + removed + ")"); - else if (toRemove == removed) - console.log("[Console] Kicked " + toRemove + " bots"); - else - console.log("[Console] Only " + removed + " bots could be kicked"); - }, - board: function(gameServer, split) { - var newLB = []; - for (var i = 1; i < split.length; i++) { - if (split[i]) { - newLB[i - 1] = split[i]; - } else { - newLB[i - 1] = " "; - } - } - - // Clears the update leaderboard function and replaces it with our own - gameServer.gameMode.packetLB = 48; - gameServer.gameMode.specByLeaderboard = false; - gameServer.gameMode.updateLB = function(gameServer) { - gameServer.leaderboard = newLB; - }; - console.log("[Console] Successfully changed leaderboard values"); - }, - boardreset: function(gameServer) { - // Gets the current gamemode - var gm = GameMode.get(gameServer.gameMode.ID); - - // Replace functions - gameServer.gameMode.packetLB = gm.packetLB; - gameServer.gameMode.updateLB = gm.updateLB; - console.log("[Console] Successfully reset leaderboard"); - }, - change: function(gameServer, split) { - var key = split[1]; - var value = split[2]; - - // Check if int/float - if (value.indexOf('.') != -1) { - value = parseFloat(value); - } else { - value = parseInt(value); - } - - if (typeof gameServer.config[key] != 'undefined') { - gameServer.config[key] = value; - console.log("[Console] Set " + key + " to " + value); - } else { - console.log("[Console] Invalid config value"); - } - }, - clear: function() { - process.stdout.write("\u001b[2J\u001b[0;0H"); - }, - color: function(gameServer, split) { - // Validation checks - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - var color = { - r: 0, - g: 0, - b: 0 - }; - color.r = Math.max(Math.min(parseInt(split[2]), 255), 0); - color.g = Math.max(Math.min(parseInt(split[3]), 255), 0); - color.b = Math.max(Math.min(parseInt(split[4]), 255), 0); - - // Sets color to the specified amount - for (var i in gameServer.clients) { - if (gameServer.clients[i].playerTracker.pID == id) { - var client = gameServer.clients[i].playerTracker; - client.setColor(color); // Set color - for (var j in client.cells) { - client.cells[j].setColor(color); - } - break; - } - } - }, - exit: function(gameServer, split) { - console.log("[Console] Closing server..."); - gameServer.socketServer.close(); - process.exit(1); - }, - food: function(gameServer, split) { - var pos = { - x: parseInt(split[1]), - y: parseInt(split[2]) - }; - var mass = parseInt(split[3]); - - // Make sure the input values are numbers - if (isNaN(pos.x) || isNaN(pos.y)) { - console.log("[Console] Invalid coordinates"); - return; - } - - if (isNaN(mass)) { - mass = gameServer.config.foodStartMass; - } - - // Spawn - var f = new Entity.Food(gameServer.getNextNodeId(), null, pos, mass, gameServer); - f.setColor(gameServer.getRandomColor()); - gameServer.addNode(f); - gameServer.currentFood++; - console.log("[Console] Spawned 1 food cell at (" + pos.x + " , " + pos.y + ")"); - }, - gamemode: function(gameServer, split) { - try { - var n = parseInt(split[1]); - var gm = GameMode.get(n); // If there is an invalid gamemode, the function will exit - gameServer.gameMode.onChange(gameServer); // Reverts the changes of the old gamemode - gameServer.gameMode = gm; // Apply new gamemode - gameServer.gameMode.onServerInit(gameServer); // Resets the server - console.log("[Game] Changed game mode to " + gameServer.gameMode.name); - } catch (e) { - console.log("[Console] Invalid game mode selected"); - } - }, - kick: function(gameServer, split) { - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - for (var i in gameServer.clients) { - if (gameServer.clients[i].playerTracker.pID == id) { - var client = gameServer.clients[i].playerTracker; - var len = client.cells.length; - for (var j = 0; j < len; j++) { - gameServer.removeNode(client.cells[0]); - } - client.socket.close(); - console.log("[Console] Kicked " + client.name); - break; - } - } - }, - kickall: function(gameServer, split) { - for (var i in gameServer.clients) { - var client = gameServer.clients[i].playerTracker; - var len = client.cells.length; - for (var j = 0; j < len; j++) { - gameServer.removeNode(client.cells[0]); - } - client.socket.close(); - console.log("[Console] Kicked " + client.name); - } - }, - kill: function(gameServer, split) { - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - var count = 0; - for (var i in gameServer.clients) { - if (gameServer.clients[i].playerTracker.pID == id) { - var client = gameServer.clients[i].playerTracker; - var len = client.cells.length; - for (var j = 0; j < len; j++) { - gameServer.removeNode(client.cells[0]); - count++; - } - - console.log("[Console] Removed " + count + " cells"); - break; - } - } - }, - killall: function(gameServer, split) { - var count = 0; - var len = gameServer.nodesPlayer.length; - for (var i = 0; i < len; i++) { - gameServer.removeNode(gameServer.nodesPlayer[0]); - count++; - } - console.log("[Console] Removed " + count + " cells"); - }, - mass: function(gameServer, split) { - // Validation checks - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - var amount = Math.max(parseInt(split[2]), 9); - if (isNaN(amount)) { - console.log("[Console] Please specify a valid number"); - return; - } - - // Sets mass to the specified amount - for (var i in gameServer.clients) { - if (gameServer.clients[i].playerTracker.pID == id) { - var client = gameServer.clients[i].playerTracker; - for (var j in client.cells) { - client.cells[j].mass = amount; - } - - console.log("[Console] Set mass of " + client.name + " to " + amount); - break; - } - } - }, - merge: function(gameServer, split) { - // Validation checks - var id = parseInt(split[1]); - var set = split[2]; - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - // Find client with same ID as player entered - var client; - for (var i = 0; i < gameServer.clients.length; i++) { - if (id == gameServer.clients[i].playerTracker.pID) { - client = gameServer.clients[i].playerTracker; - break; - } - } - - if (!client) { - console.log("[Console] Client is nonexistent!"); - return; - } - - if (client.cells.length == 1) { - console.log("[Console] Client already has one cell!"); - return; - } - - // Set client's merge override - var state; - if (set == "true") { - client.mergeOverride = true; - client.mergeOverrideDuration = 100; - state = true; - } else if (set == "false") { - client.mergeOverride = false; - client.mergeOverrideDuration = 0; - state = false; - } else { - if (client.mergeOverride) { - client.mergeOverride = false; - client.mergeOverrideDuration = 0; - } else { - client.mergeOverride = true; - client.mergeOverrideDuration = 100; - } - - state = client.mergeOverride; - } - - // Log - if (state) console.log("[Console] Player " + id + " is now force merging"); - else console.log("[Console] Player " + id + " isn't force merging anymore"); - }, - name: function(gameServer, split) { - // Validation checks - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - var name = split.slice(2, split.length).join(' '); - if (typeof name == 'undefined') { - console.log("[Console] Please type a valid name"); - return; - } - - // Change name - for (var i = 0; i < gameServer.clients.length; i++) { - var client = gameServer.clients[i].playerTracker; - - if (client.pID == id) { - console.log("[Console] Changing " + client.name + " to " + name); - client.name = name; - return; - } - } - - // Error - console.log("[Console] Player " + id + " was not found"); - }, - playerlist: function(gameServer, split) { - console.log("[Console] Showing " + gameServer.clients.length + " players: "); - console.log(" ID | IP | " + fillChar('NICK', ' ', gameServer.config.playerMaxNickLength) + " | CELLS | SCORE | POSITION "); // Fill space - console.log(fillChar('', '-', ' ID | IP | | CELLS | SCORE | POSITION '.length + gameServer.config.playerMaxNickLength)); - for (var i = 0; i < gameServer.clients.length; i++) { - var client = gameServer.clients[i].playerTracker; - - // ID with 3 digits length - var id = fillChar((client.pID), ' ', 10, true); - - // Get ip (15 digits length) - var ip = "BOT"; - if (typeof gameServer.clients[i].remoteAddress != 'undefined') { - ip = gameServer.clients[i].remoteAddress; - } - ip = fillChar(ip, ' ', 15); - - // Get name and data - var nick = '', - cells = '', - score = '', - position = '', - data = ''; - if (client.spectate) { - try { - nick = gameServer.largestClient.name; - } catch (e) { - // Specating in free-roam mode - nick = "in free-roam"; - } - nick = (nick == "") ? "An unnamed cell" : nick; - data = fillChar("SPECTATING: " + nick, '-', ' | CELLS | SCORE | POSITION '.length + gameServer.config.playerMaxNickLength, true); - console.log(" " + id + " | " + ip + " | " + data); - } else if (client.cells.length > 0) { - nick = fillChar((client.name == "") ? "An unnamed cell" : client.name, ' ', gameServer.config.playerMaxNickLength); - cells = fillChar(client.cells.length, ' ', 5, true); - score = fillChar(client.getScore(true), ' ', 6, true); - position = fillChar(client.centerPos.x >> 0, ' ', 5, true) + ', ' + fillChar(client.centerPos.y >> 0, ' ', 5, true); - console.log(" " + id + " | " + ip + " | " + nick + " | " + cells + " | " + score + " | " + position); - } else { - // No cells = dead player or in-menu - data = fillChar('DEAD OR NOT PLAYING', '-', ' | CELLS | SCORE | POSITION '.length + gameServer.config.playerMaxNickLength, true); - console.log(" " + id + " | " + ip + " | " + data); - } - } - }, - pause: function(gameServer, split) { - gameServer.run = !gameServer.run; // Switches the pause state - var s = gameServer.run ? "Unpaused" : "Paused"; - console.log("[Console] " + s + " the game."); - }, - reload: function(gameServer) { - gameServer.loadConfig(); - console.log("[Console] Reloaded the config file successfully"); - }, - resetantiteam: function(gameServer, split) { - // Validation checks - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - for (var i in gameServer.clients) { - var client = gameServer.clients[i]; - if (!client) continue; // Nonexistent - - if (client.playerTracker.pID == id) { - // Found client - client.playerTracker.massDecayMult = 1; - client.playerTracker.Wmult = 0; - client.playerTracker.virusMult = 0; - client.playerTracker.splittingMult = 0; - console.log("[Console] Successfully reset client's anti-team effect"); - return; - } - } - }, - status: function(gameServer, split) { - // Get amount of humans/bots - var humans = 0, - bots = 0; - for (var i = 0; i < gameServer.clients.length; i++) { - if ('_socket' in gameServer.clients[i]) { - humans++; - } else { - bots++; - } - } - // - console.log("[Console] Connected players: " + gameServer.clients.length + "/" + gameServer.config.serverMaxConnections); - console.log("[Console] Players: " + humans + " - Bots: " + bots); - console.log("[Console] Server has been running for " + Math.floor(process.uptime()/60) + " minutes"); - console.log("[Console] Current memory usage: " + Math.round(process.memoryUsage().heapUsed / 1048576 * 10)/10 + "/" + Math.round(process.memoryUsage().heapTotal / 1048576 * 10)/10 + " mb"); - console.log("[Console] Current game mode: " + gameServer.gameMode.name); - }, - tp: function(gameServer, split) { - var id = parseInt(split[1]); - if (isNaN(id)) { - console.log("[Console] Please specify a valid player ID!"); - return; - } - - // Make sure the input values are numbers - var pos = { - x: parseInt(split[2]), - y: parseInt(split[3]) - }; - if (isNaN(pos.x) || isNaN(pos.y)) { - console.log("[Console] Invalid coordinates"); - return; - } - - // Spawn - for (var i in gameServer.clients) { - if (gameServer.clients[i].playerTracker.pID == id) { - var client = gameServer.clients[i].playerTracker; - for (var j in client.cells) { - client.cells[j].position.x = pos.x; - client.cells[j].position.y = pos.y; - } - - console.log("[Console] Teleported " + client.name + " to (" + pos.x + " , " + pos.y + ")"); - break; - } - } - }, - virus: function(gameServer, split) { - var pos = { - x: parseInt(split[1]), - y: parseInt(split[2]) - }; - var mass = parseInt(split[3]); - - // Make sure the input values are numbers - if (isNaN(pos.x) || isNaN(pos.y)) { - console.log("[Console] Invalid coordinates"); - return; - } - if (isNaN(mass)) { - mass = gameServer.config.virusStartMass; - } - - // Spawn - var v = new Entity.Virus(gameServer.getNextNodeId(), null, pos, mass); - gameServer.addNode(v); - console.log("[Console] Spawned 1 virus at (" + pos.x + " , " + pos.y + ")"); - }, - //Aliases - st: function (gameServer, split) { - Commands.list.status(gameServer, split); - }, - pl: function(gameServer, split){ - Commands.list.playerlist(gameServer, split); - } -}; +// Imports +var GameMode = require('../gamemodes'); +var Entity = require('../entity'); +var ini = require('./ini.js'); +var Logger = require('./Logger'); +var heapdump = null; + +function Commands() { + this.list = {}; // Empty +} + +module.exports = Commands; + +// Utils +var fillChar = function (data, char, fieldLength, rTL) { + var result = data.toString(); + if (rTL === true) { + for (var i = result.length; i < fieldLength; i++) + result = char.concat(result); + } else { + for (var i = result.length; i < fieldLength; i++) + result = result.concat(char); + } + return result; +}; + +// Commands + +Commands.list = { + help: function (gameServer, split) { + console.log("======================== HELP ======================"); + console.log("addbot [number] : add bot to the server"); + console.log("kickbot [number] : kick a number of bots"); + console.log("ban [PlayerID | IP] : bans a(n) (player's) IP"); + console.log("banlist : get list of banned IPs."); + console.log("board [string] [string] ... : set scoreboard text"); + console.log("boardreset : reset scoreboard text"); + console.log("change [setting] [value] : change specified settings"); + console.log("clear : clear console output"); + console.log("color [PlayerID] [R] [G] [B] : set cell(s) color by client ID"); + console.log("exit : stop the server"); + console.log("food [X] [Y] [mass] : spawn food at specified Location"); + console.log("gamemode [id] : change server gamemode"); + console.log("kick [PlayerID] : kick player or bot by client ID"); + console.log("kickall : kick all players and bots"); + console.log("mute [PlayerID] : mute player (block chat messages from him)"); + console.log("unmute [PlayerID] : unmute player (allow chat messages from him)"); + console.log("kill [PlayerID] : kill cell(s) by client ID"); + console.log("killall : kill everyone"); + console.log("mass [PlayerID] [mass] : set cell(s) mass by client ID"); + console.log("merge [PlayerID] : merge all client's cells once"); + console.log("skin [PlayerID] [SkinName] : change player skin"); + console.log("name [PlayerID] [name] : change cell(s) name by client ID"); + console.log("playerlist : get list of players and bots"); + console.log("pause : pause game , freeze all cells"); + console.log("reload : reload config"); + console.log("status : get server status"); + console.log("tp [PlayerID] [X] [Y] : teleport player to specified location"); + console.log("unban [IP] : unban an IP"); + console.log("virus [X] [Y] [mass] : spawn virus at a specified Location"); + console.log("pl : alias for playerlist"); + console.log("st : alias for status"); + console.log("===================================================="); + }, + debug: function (gameServer, split) { + // Used for checking node lengths (for now) + + // Count client cells + var clientCells = 0; + for (var i in gameServer.clients) { + clientCells += gameServer.clients[i].playerTracker.cells.length; + } + // Output node information + console.log("Clients: " + fillChar(gameServer.clients.length, " ", 4, true) + " / " + gameServer.config.serverMaxConnections + " + bots"); + console.log("Total nodes:" + fillChar(gameServer.nodes.length, " ", 8, true)); + console.log("- Client cells: " + fillChar(clientCells, " ", 4, true) + " / " + (gameServer.clients.length * gameServer.config.playerMaxCells)); + console.log("- Ejected cells:" + fillChar(gameServer.nodesEjected.length, " ", 4, true)); + console.log("- Foods: " + fillChar(gameServer.currentFood, " ", 4, true) + " / " + gameServer.config.foodMaxAmount); + console.log("- Viruses: " + fillChar(gameServer.nodesVirus.length, " ", 4, true) + " / " + gameServer.config.virusMaxAmount); + console.log("Moving nodes: " + fillChar(gameServer.movingNodes.length, " ", 4, true)); + console.log("Quad nodes: " + fillChar(gameServer.quadTree.scanNodeCount(), " ", 4, true)); + console.log("Quad items: " + fillChar(gameServer.quadTree.scanItemCount(), " ", 4, true)); + }, + addbot: function (gameServer, split) { + var add = parseInt(split[1]); + if (isNaN(add)) { + add = 1; // Adds 1 bot if user doesnt specify a number + } + + for (var i = 0; i < add; i++) { + gameServer.bots.addBot(); + } + console.log("Added " + add + " player bots"); + }, + ban: function (gameServer, split) { + // Error message + var logInvalid = "Please specify a valid player ID or IP address!"; + + if (split[1] == null) { + // If no input is given; added to avoid error + Logger.warn(logInvalid); + return; + } + + if (split[1].indexOf(".") >= 0) { + // If input is an IP address + var ip = split[1]; + var ipParts = ip.split("."); + + // Check for invalid decimal numbers of the IP address + for (var i in ipParts) { + if (i > 1 && ipParts[i] == "*") { + // mask for sub-net + continue; + } + // If not numerical or if it's not between 0 and 255 + // TODO: Catch string "e" as it means "10^". + if (isNaN(ipParts[i]) || ipParts[i] < 0 || ipParts[i] >= 256) { + Logger.warn(logInvalid); + return; + } + } + + if (ipParts.length != 4) { + // an IP without 3 decimals + Logger.warn(logInvalid); + return; + } + + gameServer.banIp(ip); + return; + } + // if input is a Player ID + var id = parseInt(split[1]); + if (isNaN(id)) { + // If not numerical + Logger.warn(logInvalid); + return; + } + var ip = null; + for (var i in gameServer.clients) { + var client = gameServer.clients[i]; + if (client == null || !client.isConnected) + continue; + if (client.playerTracker.pID == id) { + ip = client._socket.remoteAddress; + break; + } + } + if (ip) + gameServer.banIp(ip); + else + Logger.warn("Player ID " + id + " not found!"); + }, + banlist: function (gameServer, split) { + Logger.print("Showing " + gameServer.ipBanList.length + " banned IPs: "); + Logger.print(" IP | IP "); + Logger.print("-----------------------------------"); + for (var i = 0; i < gameServer.ipBanList.length; i += 2) { + Logger.print(" " + fillChar(gameServer.ipBanList[i], " ", 15) + " | " + + (gameServer.ipBanList.length === i + 1 ? "" : gameServer.ipBanList[i + 1]) + ); + } + }, + kickbot: function (gameServer, split) { + var toRemove = parseInt(split[1]); + if (isNaN(toRemove)) { + toRemove = -1; // Kick all bots if user doesnt specify a number + } + if (toRemove < 1) { + Logger.warn("Invalid argument!"); + return; + } + var removed = 0; + for (var i = 0; i < gameServer.clients.length; i++) { + var socket = gameServer.clients[i]; + if (socket.isConnected != null) continue; + socket.close(); + removed++; + if (removed >= toRemove) + break; + } + if (removed == 0) + Logger.warn("Cannot find any bots"); + else if (toRemove == removed) + Logger.warn("Kicked " + removed + " bots"); + else + Logger.warn("Only " + removed + " bots were kicked"); + }, + board: function (gameServer, split) { + var newLB = []; + for (var i = 1; i < split.length; i++) { + if (split[i]) { + newLB[i - 1] = split[i]; + } else { + newLB[i - 1] = " "; + } + } + + // Clears the update leaderboard function and replaces it with our own + gameServer.gameMode.packetLB = 48; + gameServer.gameMode.specByLeaderboard = false; + gameServer.gameMode.updateLB = function (gameServer) { + gameServer.leaderboard = newLB; + gameServer.leaderboardType = 48; + }; + console.log("Successfully changed leaderboard values"); + }, + boardreset: function (gameServer) { + // Gets the current gamemode + var gm = GameMode.get(gameServer.gameMode.ID); + + // Replace functions + gameServer.gameMode.packetLB = gm.packetLB; + gameServer.gameMode.updateLB = gm.updateLB; + console.log("Successfully reset leaderboard"); + }, + change: function (gameServer, split) { + if (split.length < 3) { + Logger.warn("Invalid command arguments"); + return; + } + var key = split[1]; + var value = split[2]; + + // Check if int/float + if (value.indexOf('.') != -1) { + value = parseFloat(value); + } else { + value = parseInt(value); + } + gameServer.changeConfig(key, value); + }, + clear: function () { + process.stdout.write("\u001b[2J\u001b[0;0H"); + }, + color: function (gameServer, split) { + // Validation checks + var id = parseInt(split[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + + var color = { + r: 0, + g: 0, + b: 0 + }; + color.r = Math.max(Math.min(parseInt(split[2]), 255), 0); + color.g = Math.max(Math.min(parseInt(split[3]), 255), 0); + color.b = Math.max(Math.min(parseInt(split[4]), 255), 0); + + // Sets color to the specified amount + for (var i in gameServer.clients) { + if (gameServer.clients[i].playerTracker.pID == id) { + var client = gameServer.clients[i].playerTracker; + client.setColor(color); // Set color + for (var j in client.cells) { + client.cells[j].setColor(color); + } + break; + } + } + }, + exit: function (gameServer, split) { + Logger.warn("Closing server..."); + gameServer.wsServer.close(); + process.exit(1); + }, + food: function (gameServer, split) { + var pos = { + x: parseInt(split[1]), + y: parseInt(split[2]) + }; + var mass = parseInt(split[3]); + + // Make sure the input values are numbers + if (isNaN(pos.x) || isNaN(pos.y)) { + Logger.warn("Invalid coordinates"); + return; + } + + var size = gameServer.config.foodMinMass; + if (!isNaN(mass)) { + size = Math.sqrt(mass * 100); + } + + // Spawn + var cell = new Entity.Food(gameServer, null, pos, size); + cell.setColor(gameServer.getRandomColor()); + gameServer.addNode(cell); + console.log("Spawned 1 food cell at (" + pos.x + " , " + pos.y + ")"); + }, + gamemode: function (gameServer, split) { + try { + var n = parseInt(split[1]); + var gm = GameMode.get(n); // If there is an invalid gamemode, the function will exit + gameServer.gameMode.onChange(gameServer); // Reverts the changes of the old gamemode + gameServer.gameMode = gm; // Apply new gamemode + gameServer.gameMode.onServerInit(gameServer); // Resets the server + console.log("Changed game mode to " + gameServer.gameMode.name); + } catch (err) { + Logger.error(err.stack); + Logger.error("Invalid game mode selected"); + } + }, + kick: function (gameServer, split) { + var id = parseInt(split[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + gameServer.kickId(id); + }, + mute: function (gameServer, args) { + if (!args || args.length < 2) { + Logger.warn("Please specify a valid player ID!"); + return; + } + var id = parseInt(args[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + var player = gameServer.getPlayerById(id); + if (player == null) { + Logger.warn("Player with id=" + id + " not found!"); + return; + } + if (player.isMuted) { + Logger.warn("Player with id=" + id + " already muted!"); + return; + } + Logger.print("Player \"" + player.getFriendlyName() + "\" were muted"); + player.isMuted = true; + }, + unmute: function (gameServer, args) { + if (!args || args.length < 2) { + Logger.warn("Please specify a valid player ID!"); + return; + } + var id = parseInt(args[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + var player = gameServer.getPlayerById(id); + if (player == null) { + Logger.warn("Player with id=" + id + " not found!"); + return; + } + if (!player.isMuted) { + Logger.warn("Player with id=" + id + " already not muted!"); + return; + } + Logger.print("Player \"" + player.getFriendlyName() + "\" were unmuted"); + player.isMuted = false; + }, + kickall: function (gameServer, split) { + gameServer.kickId(0); + }, + kill: function (gameServer, split) { + var id = parseInt(split[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + + var count = 0; + for (var i in gameServer.clients) { + if (gameServer.clients[i].playerTracker.pID == id) { + var client = gameServer.clients[i].playerTracker; + var len = client.cells.length; + for (var j = 0; j < len; j++) { + gameServer.removeNode(client.cells[0]); + count++; + } + + console.log("Removed " + count + " cells"); + break; + } + } + }, + killall: function (gameServer, split) { + var count = 0; + for (var i = 0; i < gameServer.clients.length; i++) { + var playerTracker = gameServer.clients[i].playerTracker; + while (playerTracker.cells.length > 0) { + gameServer.removeNode(playerTracker.cells[0]); + count++; + } + } + console.log("Removed " + count + " cells"); + }, + mass: function (gameServer, split) { + // Validation checks + var id = parseInt(split[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + + var amount = Math.max(parseInt(split[2]), 9); + if (isNaN(amount)) { + Logger.warn("Please specify a valid number"); + return; + } + var size = Math.sqrt(amount * 100); + + // Sets mass to the specified amount + for (var i in gameServer.clients) { + if (gameServer.clients[i].playerTracker.pID == id) { + var client = gameServer.clients[i].playerTracker; + for (var j in client.cells) { + client.cells[j].setSize(size); + } + + console.log("Set mass of " + client.getFriendlyName() + " to " + (size * size / 100).toFixed(3)); + break; + } + } + }, + skin: function (gameServer, args) { + if (!args || args.length < 3) { + Logger.warn("Please specify a valid player ID and skin name!"); + return; + } + var id = parseInt(args[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + var skin = args[2].trim(); + if (!skin) { + Logger.warn("Please specify skin name!"); + } + var player = gameServer.getPlayerById(id); + if (player == null) { + Logger.warn("Player with id=" + id + " not found!"); + return; + } + if (player.cells.length > 0) { + Logger.warn("Player is alive, skin will not be applied to existing cells"); + } + Logger.print("Player \"" + player.getFriendlyName() + "\"'s skin is changed to " + skin); + player.setSkin(skin); + }, + merge: function (gameServer, split) { + // Validation checks + var id = parseInt(split[1]); + var set = split[2]; + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + + // Find client with same ID as player entered + var client; + for (var i = 0; i < gameServer.clients.length; i++) { + if (id == gameServer.clients[i].playerTracker.pID) { + client = gameServer.clients[i].playerTracker; + break; + } + } + + if (!client) { + Logger.warn("Client is nonexistent!"); + return; + } + + if (client.cells.length == 1) { + Logger.warn("Client already has one cell!"); + return; + } + + // Set client's merge override + var state; + if (set == "true") { + client.mergeOverride = true; + client.mergeOverrideDuration = 100; + state = true; + } else if (set == "false") { + client.mergeOverride = false; + client.mergeOverrideDuration = 0; + state = false; + } else { + if (client.mergeOverride) { + client.mergeOverride = false; + client.mergeOverrideDuration = 0; + } else { + client.mergeOverride = true; + client.mergeOverrideDuration = 100; + } + + state = client.mergeOverride; + } + + // Log + if (state) console.log("Player " + id + " is now force merging"); + else console.log("Player " + id + " isn't force merging anymore"); + }, + name: function (gameServer, split) { + // Validation checks + var id = parseInt(split[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + + var name = split.slice(2, split.length).join(' '); + if (typeof name == 'undefined') { + Logger.warn("Please type a valid name"); + return; + } + + // Change name + for (var i = 0; i < gameServer.clients.length; i++) { + var client = gameServer.clients[i].playerTracker; + + if (client.pID == id) { + console.log("Changing " + client.getFriendlyName() + " to " + name); + client.setName(name); + return; + } + } + + // Error + Logger.warn("Player " + id + " was not found"); + }, + unban: function (gameServer, split) { + if (split.length < 2 || split[1] == null || split[1].trim().length < 1) { + Logger.warn("Please specify a valid IP!"); + return; + } + gameServer.unbanIp(split[1].trim()); + }, + playerlist: function (gameServer, split) { + Logger.print("Showing " + gameServer.clients.length + " players: "); + Logger.print(" ID | IP | P | " + fillChar('NICK', ' ', gameServer.config.playerMaxNickLength) + " | CELLS | SCORE | POSITION "); // Fill space + Logger.print(fillChar('', '-', ' ID | IP | | | CELLS | SCORE | POSITION '.length + gameServer.config.playerMaxNickLength)); + var sockets = gameServer.clients.slice(0); + sockets.sort(function (a, b) { return a.playerTracker.pID - b.playerTracker.pID; }); + for (var i = 0; i < sockets.length; i++) { + var socket = sockets[i]; + var client = socket.playerTracker; + + // ID with 3 digits length + var id = fillChar((client.pID), ' ', 6, true); + + // Get ip (15 digits length) + var ip = "[BOT]"; + if (socket.isConnected != null) { + ip = socket.remoteAddress; + } + ip = fillChar(ip, ' ', 15); + var protocol = gameServer.clients[i].packetHandler.protocol; + if (protocol == null) + protocol = "?" + // Get name and data + var nick = '', + cells = '', + score = '', + position = '', + data = ''; + if (socket.closeReason != null) { + // Disconnected + var reason = "[DISCONNECTED] "; + if (socket.closeReason.code) + reason += "[" + socket.closeReason.code + "] "; + if (socket.closeReason.message) + reason += socket.closeReason.message; + Logger.print(" " + id + " | " + ip + " | " + protocol + " | " + reason); + } else if (!socket.packetHandler.protocol && socket.isConnected) { + Logger.print(" " + id + " | " + ip + " | " + protocol + " | " + "[CONNECTING]"); + } else if (client.spectate) { + nick = "in free-roam"; + if (!client.freeRoam) { + var target = client.getSpectateTarget(); + if (target != null) { + nick = target.getFriendlyName(); + } + } + data = fillChar("SPECTATING: " + nick, '-', ' | CELLS | SCORE | POSITION '.length + gameServer.config.playerMaxNickLength, true); + Logger.print(" " + id + " | " + ip + " | " + protocol + " | " + data); + } else if (client.cells.length > 0) { + nick = fillChar(client.getFriendlyName(), ' ', gameServer.config.playerMaxNickLength); + cells = fillChar(client.cells.length, ' ', 5, true); + score = fillChar((client.getScore() / 100) >> 0, ' ', 6, true); + position = fillChar(client.centerPos.x >> 0, ' ', 5, true) + ', ' + fillChar(client.centerPos.y >> 0, ' ', 5, true); + Logger.print(" " + id + " | " + ip + " | " + protocol + " | " + nick + " | " + cells + " | " + score + " | " + position); + } else { + // No cells = dead player or in-menu + data = fillChar('DEAD OR NOT PLAYING', '-', ' | CELLS | SCORE | POSITION '.length + gameServer.config.playerMaxNickLength, true); + Logger.print(" " + id + " | " + ip + " | " + protocol + " | " + data); + } + } + }, + pause: function (gameServer, split) { + gameServer.run = !gameServer.run; // Switches the pause state + var s = gameServer.run ? "Unpaused" : "Paused"; + console.log(s + " the game."); + }, + reload: function (gameServer) { + gameServer.loadConfig(); + gameServer.loadIpBanList(); + console.log("Reloaded the config file successfully"); + }, + status: function (gameServer, split) { + // Get amount of humans/bots + var humans = 0, + bots = 0; + for (var i = 0; i < gameServer.clients.length; i++) { + if ('_socket' in gameServer.clients[i]) { + humans++; + } else { + bots++; + } + } + + console.log("Connected players: " + gameServer.clients.length + "/" + gameServer.config.serverMaxConnections); + console.log("Players: " + humans + " - Bots: " + bots); + console.log("Server has been running for " + Math.floor(process.uptime() / 60) + " minutes"); + console.log("Current memory usage: " + Math.round(process.memoryUsage().heapUsed / 1048576 * 10) / 10 + "/" + Math.round(process.memoryUsage().heapTotal / 1048576 * 10) / 10 + " mb"); + console.log("Current game mode: " + gameServer.gameMode.name); + console.log("Current update time: " + gameServer.updateTimeAvg.toFixed(3) + " [ms] (" + ini.getLagMessage(gameServer.updateTimeAvg) + ")"); + }, + tp: function (gameServer, split) { + var id = parseInt(split[1]); + if (isNaN(id)) { + Logger.warn("Please specify a valid player ID!"); + return; + } + + // Make sure the input values are numbers + var pos = { + x: parseInt(split[2]), + y: parseInt(split[3]) + }; + if (isNaN(pos.x) || isNaN(pos.y)) { + Logger.warn("Invalid coordinates"); + return; + } + + // Spawn + for (var i in gameServer.clients) { + if (gameServer.clients[i].playerTracker.pID == id) { + var client = gameServer.clients[i].playerTracker; + for (var j in client.cells) { + client.cells[j].setPosition(pos); + gameServer.updateNodeQuad(client.cells[j]); + } + + console.log("Teleported " + client.getFriendlyName() + " to (" + pos.x + " , " + pos.y + ")"); + break; + } + } + }, + virus: function (gameServer, split) { + var pos = { + x: parseInt(split[1]), + y: parseInt(split[2]) + }; + var mass = parseInt(split[3]); + + // Make sure the input values are numbers + if (isNaN(pos.x) || isNaN(pos.y)) { + Logger.warn("Invalid coordinates"); + return; + } + var size = gameServer.config.virusMinSize; + if (!isNaN(mass)) { + size = Math.sqrt(mass * 100); + } + + // Spawn + var v = new Entity.Virus(gameServer, null, pos, size); + gameServer.addNode(v); + console.log("Spawned 1 virus at (" + pos.x + " , " + pos.y + ")"); + }, + //Aliases + st: function (gameServer, split) { + Commands.list.status(gameServer, split); + }, + pl: function (gameServer, split) { + Commands.list.playerlist(gameServer, split); + }, + heapdump: function (gameServer, args) { + if (heapdump == null) { + function tryLoadModule(name) { + try { + return require(name); + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') + return null; + Logger.error(err); + } + return null; + } + heapdump = tryLoadModule('heapdump'); + } + if (heapdump == null) { + Logger.warn("heapdump module not installed!"); + return; + } + heapdump.writeSnapshot(function (err, filename) { + if (err) + Logger.error(err); + else + Logger.print('heapdump written to ' + filename); + }); + } +}; diff --git a/src/modules/Logger.js b/src/modules/Logger.js new file mode 100644 index 000000000..fce97ab72 --- /dev/null +++ b/src/modules/Logger.js @@ -0,0 +1,265 @@ +'use strict'; +/* + * Simple logger. + * + * Copyright (c) 2016 Barbosik https://github.com/Barbosik + * License: Apache License, Version 2.0 + * + */ + +var fs = require("fs"); +var util = require('util'); +var EOL = require('os').EOL; +var LogLevelEnum = require('../enum/LogLevelEnum'); + + +module.exports.debug = debug; +module.exports.info = info; +module.exports.warn = warn; +module.exports.error = error; +module.exports.fatal = fatal; +module.exports.print = print; +module.exports.write = write; +module.exports.writeDebug = writeDebug; +module.exports.writeError = writeError; +module.exports.start = start; +module.exports.shutdown = shutdown; +module.exports.setVerbosity = function (level) { + logVerbosity = level; +}; +module.exports.setFileVerbosity = function (level) { + logFileVerbosity = level; +}; +module.exports.getVerbosity = function () { + return logVerbosity; +}; +module.exports.getFileVerbosity = function () { + return logFileVerbosity; +}; + + +var logVerbosity = LogLevelEnum.DEBUG; +var logFileVerbosity = LogLevelEnum.DEBUG; + +function debug(message) { + writeCon(colorWhite, LogLevelEnum.DEBUG, message); + writeLog(LogLevelEnum.DEBUG, message); +}; + +function info(message) { + writeCon(colorWhite + colorBright, LogLevelEnum.INFO, message); + writeLog(LogLevelEnum.INFO, message); +}; + +function warn(message) { + writeCon(colorYellow + colorBright, LogLevelEnum.WARN, message); + writeLog(LogLevelEnum.WARN, message); +}; + +function error(message) { + writeCon(colorRed + colorBright, LogLevelEnum.ERROR, message); + writeLog(LogLevelEnum.ERROR, message); +}; + +function fatal(message) { + writeCon(colorRed + colorBright, LogLevelEnum.FATAL, message); + writeLog(LogLevelEnum.FATAL, message); +}; + +function print(message) { + writeCon(colorWhite, LogLevelEnum.NONE, message); + writeLog(LogLevelEnum.NONE, message); +}; + +function write(message) { + writeLog(LogLevelEnum.NONE, message); +}; + +function writeDebug(message) { + writeLog(LogLevelEnum.DEBUG, message); +}; + +function writeError(message) { + writeLog(LogLevelEnum.ERROR, message); +}; + + +// --- utils --- + +function getDateTimeString() { + var date = new Date(); + var dy = date.getFullYear(); + var dm = date.getMonth() + 1; + var dd = date.getDate(); + var th = date.getHours(); + var tm = date.getMinutes(); + var ts = date.getSeconds(); + var tz = date.getMilliseconds(); + dy = ("0000" + dy).slice(-4); + dm = ("00" + dm).slice(-2); + dd = ("00" + dd).slice(-2); + th = ("00" + th).slice(-2); + tm = ("00" + tm).slice(-2); + ts = ("00" + ts).slice(-2); + tz = ("000" + tz).slice(-3); + return dy + "-" + dm + "-" + dd + "T" + th + "-" + tm + "-" + ts + "-" + tz; +}; + +function getTimeString() { + var date = new Date(); + var th = date.getHours(); + var tm = date.getMinutes(); + var ts = date.getSeconds(); + th = ("00" + th).slice(-2); + tm = ("00" + tm).slice(-2); + ts = ("00" + ts).slice(-2); + return th + ":" + tm + ":" + ts; +}; + +function writeCon(color, level, message) { + if (level > logVerbosity) return; + message = util.format(message); + var prefix = ""; + if (level == LogLevelEnum.DEBUG) + prefix = "[DEBUG] "; + else if (level == LogLevelEnum.INFO) + prefix = "[INFO ] "; + else if (level == LogLevelEnum.WARN) + prefix = "[WARN ] "; + else if (level == LogLevelEnum.ERROR) + prefix = "[ERROR] "; + else if (level == LogLevelEnum.FATAL) + prefix = "[FATAL] "; + process.stdout.write(color + prefix + message + "\u001B[0m" + EOL); +}; + +function writeLog(level, message) { + if (level > logFileVerbosity || writeError) + return; + message = util.format(message); + var prefix = ""; + if (level == LogLevelEnum.DEBUG) + prefix = "[DEBUG]"; + else if (level == LogLevelEnum.INFO) + prefix = "[INFO ]"; + else if (level == LogLevelEnum.WARN) + prefix = "[WARN ]"; + else if (level == LogLevelEnum.ERROR) + prefix = "[ERROR]"; + else if (level == LogLevelEnum.FATAL) + prefix = "[FATAL]"; + else if (level == LogLevelEnum.NONE) + prefix = "[NONE ]"; + prefix += "[" + getTimeString() + "] "; + + writeQueue.push(prefix + message + EOL); + if (writeShutdown) { + flushSync(); + } else { + if (writeCounter == 0) { + flushAsync(); + } + } +}; + +var writeError = false; +var writeCounter = 0; +var writeShutdown = false; +var writeStarted = false; +var writeQueue = []; + +function flushAsync() { + if (writeShutdown || consoleLog == null || writeQueue.length == 0) + return; + writeCounter++; + consoleLog.write(writeQueue.shift(), function () { writeCounter--; flushAsync(); }); +}; + +function flushSync() { + try { + var tail = ""; + while (writeQueue.length > 0) { + tail += writeQueue.shift(); + } + var fileName = logFolder + "/" + logFileName + ".log"; + fs.appendFileSync(fileName, tail); + } catch (err) { + writeError = true; + writeCon(colorRed + colorBright, LogLevelEnum.ERROR, err.message); + writeCon(colorRed + colorBright, LogLevelEnum.ERROR, "Failed to append log file!"); + } +}; + +function start() { + if (writeStarted) + return; + writeStarted = true; + try { + console.log = function (message) { print(message); }; + + var timeString = getDateTimeString(); + var fileName = logFolder + "/" + logFileName + ".log"; + var fileName2 = logBackupFolder + "/" + logFileName + "-" + timeString + ".log"; + + if (!fs.existsSync(logFolder)) { + // Make log folder + fs.mkdirSync(logFolder); + } else if (fs.existsSync(fileName)) { + if (!fs.existsSync(logBackupFolder)) { + // Make log backup folder + fs.mkdirSync(logBackupFolder); + } + // Backup previous log + fs.renameSync(fileName, fileName2); + } + + fs.writeFileSync(fileName, "=== Started " + timeString + " ===" + EOL); + var file = fs.createWriteStream(fileName, { flags: 'a' }); + file.on('open', function () { + if (writeShutdown) { + file.close(); + return; + } + consoleLog = file; + flushAsync(); + }); + file.on('error', function (err) { + writeError = true; + consoleLog = null; + writeCon(colorRed + colorBright, LogLevelEnum.ERROR, err.message); + }); + } catch (err) { + writeError = true; + consoleLog = null; + writeCon(colorRed + colorBright, LogLevelEnum.ERROR, err.message); + } +} + +function shutdown() { + writeShutdown = true; + if (writeError) return; + if (consoleLog != null) { + consoleLog.end(); + consoleLog.close(); + consoleLog.destroy(); + consoleLog = null; + } + writeQueue.push("=== Shutdown " + getDateTimeString() + " ===" + EOL); + flushSync(); +}; + + +var logFolder = "./logs"; +var logBackupFolder = "./logs/LogBackup"; +var logFileName = "MultiOgar"; + +var consoleLog = null; +var colorBlack = "\u001B[30m"; +var colorRed = "\u001B[31m"; +var colorGreen = "\u001B[32m"; +var colorYellow = "\u001B[33m"; +var colorBlue = "\u001B[34m"; +var colorMagenta = "\u001B[35m"; +var colorCyan = "\u001B[36m"; +var colorWhite = "\u001B[37m"; +var colorBright = "\u001B[1m"; diff --git a/src/modules/PlayerCommand.js b/src/modules/PlayerCommand.js new file mode 100644 index 000000000..273ce86c8 --- /dev/null +++ b/src/modules/PlayerCommand.js @@ -0,0 +1,129 @@ +var Entity = require('../entity'); +var Logger = require('./Logger'); +var UserRoleEnum = require("../enum/UserRoleEnum"); + + +var ErrorTextInvalidCommand = "ERROR: Unknown command, type /help for command list"; +var ErrorTextBadCommand = "ERROR: Bad command!"; + + +function PlayerCommand(gameServer, playerTracker) { + this.gameServer = gameServer; + this.playerTracker = playerTracker; +} + +module.exports = PlayerCommand; + +PlayerCommand.prototype.writeLine = function (text) { + this.gameServer.sendChatMessage(null, this.playerTracker, text); +}; + +PlayerCommand.prototype.executeCommandLine = function (commandLine) { + if (!commandLine) return; + var command = commandLine; + var args = ""; + var index = commandLine.indexOf(' '); + if (index >= 0) { + command = commandLine.slice(0, index); + args = commandLine.slice(index + 1, commandLine.length); + } + command = command.trim().toLowerCase(); + if (command.length > 16) { + this.writeLine(ErrorTextInvalidCommand); + return; + } + for (var i = 0; i < command.length; i++) { + var c = command.charCodeAt(i); + if (c < 0x21 || c > 0x7F) { + this.writeLine(ErrorTextInvalidCommand); + return; + } + } + if (!playerCommands.hasOwnProperty(command)) { + this.writeLine(ErrorTextInvalidCommand); + return; + } + var execute = playerCommands[command]; + if (typeof execute == 'function') { + execute.bind(this)(args); + } else { + this.writeLine(ErrorTextBadCommand); + } +}; + +var playerCommands = { + help: function (args) { + this.writeLine("/skin %shark - change skin"); + this.writeLine("/kill - self kill"); + this.writeLine("/help - this command list"); + }, + skin: function (args) { + if (this.playerTracker.cells.length > 0) { + this.writeLine("ERROR: Cannot change skin while player in game!"); + return; + } + var skinName = ""; + if (args) skinName = args.trim(); + if (!this.gameServer.checkSkinName(skinName)) { + this.writeLine("ERROR: Invalid skin name!"); + return; + } + this.playerTracker.setSkin(skinName); + if (skinName == "") + this.writeLine("Your skin was removed"); + else + this.writeLine("Your skin set to " + skinName); + }, + kill: function (args) { + if (this.playerTracker.cells.length < 1) { + this.writeLine("You cannot kill yourself, because you're still not joined to the game!"); + return; + } + while (this.playerTracker.cells.length > 0) { + var cell = this.playerTracker.cells[0]; + this.gameServer.removeNode(cell); + // replace with food + var food = new Entity.Food(this.gameServer, null, cell.position, this.gameServer.config.playerMinSize); + food.setColor(this.gameServer.getGrayColor(cell.getColor())); + this.gameServer.addNode(food); + } + this.writeLine("You killed yourself"); + }, + login: function (args) { + var password = (args || "").trim(); + if (password.length < 1) { + this.writeLine("ERROR: missing password argument!"); + return; + } + var user = this.gameServer.userLogin(this.playerTracker.socket.remoteAddress, password); + if (!user) { + this.writeLine("ERROR: login failed!"); + return; + } + Logger.write("LOGIN " + this.playerTracker.socket.remoteAddress + ":" + this.playerTracker.socket.remotePort + " as \"" + user.name + "\""); + this.playerTracker.userRole = user.role; + this.playerTracker.userAuth = user.name; + this.writeLine("Login done as \"" + user.name + "\""); + return; + }, + logout: function (args) { + if (this.playerTracker.userRole == UserRoleEnum.GUEST) { + this.writeLine("ERROR: not logged in"); + return; + } + Logger.write("LOGOUT " + this.playerTracker.socket.remoteAddress + ":" + this.playerTracker.socket.remotePort + " as \"" + this.playerTracker.userAuth + "\""); + this.playerTracker.userRole = UserRoleEnum.GUEST; + this.playerTracker.userAuth = null; + this.writeLine("Logout done"); + }, + shutdown: function (args) { + if (this.playerTracker.userRole != UserRoleEnum.ADMIN) { + this.writeLine("ERROR: access denied!"); + return; + } + Logger.warn("SHUTDOWN REQUEST FROM " + this.playerTracker.socket.remoteAddress + " as " + this.playerTracker.userAuth); + process.exit(0); + } +}; + + diff --git a/src/modules/ini.js b/src/modules/ini.js index c01ddb7a2..5f277ed48 100644 --- a/src/modules/ini.js +++ b/src/modules/ini.js @@ -1,190 +1,227 @@ -exports.parse = exports.decode = decode; -exports.stringify = exports.encode = encode; - -exports.safe = safe; -exports.unsafe = unsafe; - -var eol = process.platform === "win32" ? "\r\n" : "\n"; - -function encode(obj, opt) { - var children = [], - out = ""; - - if (typeof opt === "string") { - opt = { - section: opt, - whitespace: false - }; - } else { - opt = opt || {}; - opt.whitespace = opt.whitespace === true; - } - - var separator = " = "; - - Object.keys(obj).forEach(function(k, _, __) { - var val = obj[k]; - if (val && Array.isArray(val)) { - val.forEach(function(item) { - out += safe(k + "[]") + separator + safe(item) + "\n"; - }); - } else if (val && typeof val === "object") { - children.push(k); - } else { - out += safe(k) + separator + safe(val) + eol; - } - }); - - if (opt.section && out.length) { - out = "[" + safe(opt.section) + "]" + eol + out; - } - - children.forEach(function(k, _, __) { - var nk = dotSplit(k).join('\\.'); - var section = (opt.section ? opt.section + "." : "") + nk; - var child = encode(obj[k], { - section: section, - whitespace: opt.whitespace - }); - if (out.length && child.length) { - out += eol; - } - out += child; - }); - - return out; -} - -function dotSplit(str) { - return str.replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002') - .replace(/\\\./g, '\u0001') - .split(/\./).map(function(part) { - return part.replace(/\1/g, '\\.') - .replace(/\2LITERAL\\1LITERAL\2/g, '\u0001'); - }); -} - -function decode(str) { - var out = {}, - p = out, - state = "START", - // section |key = value - re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i, - lines = str.split(/[\r\n]+/g), - section = null; - - lines.forEach(function(line, _, __) { - if (!line || line.match(/^\s*[;#]/)) { - return; - } - - var match = line.match(re); - - if (!match) { - return; - } - - if (match[1] !== undefined) { - section = unsafe(match[1]); - p = out[section] = out[section] || {}; - return; - } - - var key = unsafe(match[2]), - value = match[3] ? unsafe((match[4] || "")) : true; - - // Convert keys with '[]' suffix to an array - if (key.length > 2 && key.slice(-2) === "[]") { - key = key.substring(0, key.length - 2); - if (!p[key]) { - p[key] = []; - } else if (!Array.isArray(p[key])) { - p[key] = [p[key]]; - } - } - - // safeguard against resetting a previously defined - // array by accidentally forgetting the brackets - if (isInt(value)) { - p[key] = parseInt(value); - } else { - p[key] = parseFloat(value); - } - }); - - // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} - // use a filter to return the keys that have to be deleted. - Object.keys(out).filter(function(k, _, __) { - if (!out[k] || typeof out[k] !== "object" || Array.isArray(out[k])) return false; - // see if the parent section is also an object. - // if so, add it to that, and mark this one for deletion - var parts = dotSplit(k), - p = out, - l = parts.pop(), - nl = l.replace(/\\\./g, '.'); - parts.forEach(function(part, _, __) { - if (!p[part] || typeof p[part] !== "object") { - p[part] = {}; - } - p = p[part]; - }); - if (p === out && nl === l) { - return false; - } - p[nl] = out[k]; - return true; - }).forEach(function(del, _, __) { - delete out[del]; - }); - - return out; -} - -function isQuoted(val) { - return (val.charAt(0) === "\"" && val.slice(-1) === "\"") || (val.charAt(0) === "'" && val.slice(-1) === "'"); -} - -function safe(val) { - return (typeof val !== "string" || val.match(/[=\r\n]/) || val.match(/^\[/) || (val.length > 1 && isQuoted(val)) || val !== val.trim()) ? JSON.stringify(val) : val.replace(/;/g, '\\;').replace(/#/g, "\\#"); -} - -function unsafe(val, doUnesc) { - val = (val || "").trim(); - if (isQuoted(val)) { - // remove the single quotes before calling JSON.parse - if (val.charAt(0) === "'") { - val = val.substr(1, val.length - 2); - } - try { - val = JSON.parse(val); - } catch (_) {} - } else { - // walk the val to find the first not-escaped ; character - var esc = false; - var unesc = ""; - for (var i = 0, l = val.length; i < l; i++) { - var c = val.charAt(i); - if (esc) { - if ("\\;#".indexOf(c) !== -1) - unesc += c; - else - unesc += "\\" + c; - esc = false; - } else if (";#".indexOf(c) !== -1) { - break; - } else if (c === "\\") { - esc = true; - } else { - unesc += c; - } - } - if (esc) - unesc += "\\"; - return unesc; - } - return val; -} - -var isInt = function(n) { - return parseInt(n) === n; -}; +exports.parse = exports.decode = decode; +exports.stringify = exports.encode = encode; + +exports.safe = safe; +exports.unsafe = unsafe; +exports.getLagMessage = getLagMessage; + +var eol = process.platform === "win32" ? "\r\n" : "\n"; + +function encode(obj, opt) { + var children = [], + out = ""; + + if (typeof opt === "string") { + opt = { + section: opt, + whitespace: false + }; + } else { + opt = opt || {}; + opt.whitespace = opt.whitespace === true; + } + + var separator = " = "; + + Object.keys(obj).forEach(function (k, _, __) { + var val = obj[k]; + if (val && Array.isArray(val)) { + val.forEach(function (item) { + out += safe(k + "[]") + separator + safe(item) + "\n"; + }); + } else if (val && typeof val === "object") { + children.push(k); + } else { + out += safe(k) + separator + safe(val) + eol; + } + }); + + if (opt.section && out.length) { + out = "[" + safe(opt.section) + "]" + eol + out; + } + + children.forEach(function (k, _, __) { + var nk = dotSplit(k).join('\\.'); + var section = (opt.section ? opt.section + "." : "") + nk; + var child = encode(obj[k], { + section: section, + whitespace: opt.whitespace + }); + if (out.length && child.length) { + out += eol; + } + out += child; + }); + + return out; +} + +function dotSplit(str) { + return str.replace(/\1/g, '\u0002LITERAL\\1LITERAL\u0002') + .replace(/\\\./g, '\u0001') + .split(/\./).map(function (part) { + return part.replace(/\1/g, '\\.') + .replace(/\2LITERAL\\1LITERAL\2/g, '\u0001'); + }); +} + +function decode(str) { + var out = {}, + p = out, + state = "START", + // section |key = value + re = /^\[([^\]]*)\]$|^([^=]+)(=(.*))?$/i, + lines = str.split(/[\r\n]+/g), + section = null; + + lines.forEach(function (line, _, __) { + var testLine = line.trim(); + + // skip empty lines or commented lines + if (!line || line.match(/^\s*[;#]/)) { + // skip commented lines + return; + } + // E.g. serverTimeout = 30 + // Returns ["serverTimeout = 30", undefined, "serverTimeout ", "= 30", "30"] + var match = line.match(re); + + if (!match) { + return; + } + + if (match[1] !== undefined) { + section = unsafe(match[1]); + p = out[section] = out[section] || {}; + return; + } + + var key = unsafe(match[2]), + value = match[3] ? unsafe((match[4] || "")) : true; + + // Convert keys with '[]' suffix to an array + if (key.length > 2 && key.slice(-2) === "[]") { + key = key.substring(0, key.length - 2); + if (!p[key]) { + p[key] = []; + } else if (!Array.isArray(p[key])) { + p[key] = [p[key]]; + } + } + + //// Mass to Size function catcher + if (startsWith(value, "massToSize(") && endsWith(value, ")")) { + // 11: length of "massToSize(" + var strValue = value.slice(11, value.length - 1).trim(); + value = Math.sqrt(parseFloat(strValue) * 100) + 0.5; + } + function startsWith(value, pattern) { + return value.length >= pattern.length && + value.indexOf(pattern) === 0; + }; + function endsWith(value, pattern) { + return value.length >= pattern.length && + value.lastIndexOf(pattern) === value.length - pattern.length; + }; + + // safeguard against resetting a previously defined + // array by accidentally forgetting the brackets + if (isNaN(value)) { + p[key] = value; + } else if (isInt(value)) { + p[key] = parseInt(value); + } else { + p[key] = parseFloat(value); + } + }); + + // {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}} + // use a filter to return the keys that have to be deleted. + Object.keys(out).filter(function (k, _, __) { + if (!out[k] || typeof out[k] !== "object" || Array.isArray(out[k])) return false; + // see if the parent section is also an object. + // if so, add it to that, and mark this one for deletion + var parts = dotSplit(k), + p = out, + l = parts.pop(), + nl = l.replace(/\\\./g, '.'); + parts.forEach(function (part, _, __) { + if (!p[part] || typeof p[part] !== "object") { + p[part] = {}; + } + p = p[part]; + }); + if (p === out && nl === l) { + return false; + } + p[nl] = out[k]; + return true; + }).forEach(function (del, _, __) { + delete out[del]; + }); + + return out; +} + +function isQuoted(val) { + return (val.charAt(0) === "\"" && val.slice(-1) === "\"") || (val.charAt(0) === "'" && val.slice(-1) === "'"); +} + +function safe(val) { + return (typeof val !== "string" || val.match(/[=\r\n]/) || val.match(/^\[/) || (val.length > 1 && isQuoted(val)) || val !== val.trim()) ? JSON.stringify(val) : val.replace(/;/g, '\\;').replace(/#/g, "\\#"); +} + +function unsafe(val, doUnesc) { + val = (val || "").trim(); + if (isQuoted(val)) { + // remove the single quotes before calling JSON.parse + if (val.charAt(0) === "'") { + val = val.substr(1, val.length - 2); + } + try { + val = JSON.parse(val); + } catch (err) { + Logger.error(err.stack); + } + } else { + // walk the val to find the first not-escaped ; character + var esc = false; + var unesc = ""; + for (var i = 0, l = val.length; i < l; i++) { + var c = val.charAt(i); + if (esc) { + if ("\\;#".indexOf(c) !== -1) + unesc += c; + else + unesc += "\\" + c; + esc = false; + } else if (";#".indexOf(c) !== -1) { + break; + } else if (c === "\\") { + esc = true; + } else { + unesc += c; + } + } + if (esc) + unesc += "\\"; + return unesc; + } + return val; +} + +var isInt = function (n) { + return parseInt(n) == n; +}; + +function getLagMessage(updateTimeAvg) { + if (updateTimeAvg < 20) + return "perfectly smooth"; + if (updateTimeAvg < 35) + return "good"; + if (updateTimeAvg < 40) + return "tiny lag"; + if (updateTimeAvg < 50) + return "lag"; + return "extremely high lag"; +} diff --git a/src/modules/log.js b/src/modules/log.js deleted file mode 100644 index ec0ebdf4d..000000000 --- a/src/modules/log.js +++ /dev/null @@ -1,77 +0,0 @@ -var fs = require("fs"); -var util = require('util'); -var EOL = require('os').EOL; - -function Log() { - // Nothing -} - -module.exports = Log; - -Log.prototype.setup = function(gameServer) { - if (!fs.existsSync('./logs')) { - // Make log folder - fs.mkdir('./logs'); - } - - switch (gameServer.config.serverLogLevel) { - case 2: - ip_log = fs.createWriteStream('./logs/ip.log', { - flags: 'w' - }); - - // Override - this.onConnect = function(ip) { - ip_log.write("[" + this.formatTime() + "] Connect: " + ip + EOL); - }; - - this.onDisconnect = function(ip) { - ip_log.write("[" + this.formatTime() + "] Disconnect: " + ip + EOL); - }; - case 1: - console_log = fs.createWriteStream('./logs/console.log', { - flags: 'w' - }); - - console.log = function(d) { // - console_log.write(util.format(d) + EOL); - process.stdout.write(util.format(d) + EOL); - }; - - // - this.onCommand = function(command) { - console_log.write(">" + command + EOL); - }; - case 0: - // Prevent crashes - process.on('uncaughtException', function(err) { - console.log(err.stack); - }); - default: - break; - } -}; - -Log.prototype.onConnect = function(ip) { - // Nothing -}; - -Log.prototype.onDisconnect = function(ip) { - // Nothing -}; - -Log.prototype.onCommand = function(command) { - // Nothing -}; - -Log.prototype.formatTime = function() { - var date = new Date(); - - var hour = date.getHours(); - hour = (hour < 10 ? "0" : "") + hour; - - var min = date.getMinutes(); - min = (min < 10 ? "0" : "") + min; - - return hour + ":" + min; -}; diff --git a/src/packet/AddNode.js b/src/packet/AddNode.js index e49a24bbd..7fe644109 100644 --- a/src/packet/AddNode.js +++ b/src/packet/AddNode.js @@ -1,16 +1,13 @@ -function AddNode(item) { - this.item = item; -} - -module.exports = AddNode; - -AddNode.prototype.build = function() { - // Only add player controlled cells with this packet or it will bug the camera - var buf = new ArrayBuffer(5); - var view = new DataView(buf); - - view.setUint8(0, 32, true); - view.setUint32(1, this.item.nodeId, true); - - return buf; -}; +function AddNode(playerTracker, item) { + this.playerTracker = playerTracker; + this.item = item; +} + +module.exports = AddNode; + +AddNode.prototype.build = function (protocol) { + var buffer = new Buffer(5); + buffer.writeUInt8(0x20, 0, true); // Packet ID + buffer.writeUInt32LE((this.item.nodeId ^ this.playerTracker.scrambleId) >>> 0, 1, true); + return buffer; +}; diff --git a/src/packet/BinaryReader.js b/src/packet/BinaryReader.js new file mode 100644 index 000000000..8b4a75f20 --- /dev/null +++ b/src/packet/BinaryReader.js @@ -0,0 +1,132 @@ +'use strict'; +/* + * Simple BinaryReader is a minimal tool to read binary stream. + * Useful for binary deserialization. + * + * Copyright (c) 2016 Barbosik + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function BinaryReader(buffer) { + this._offset = 0; + this._buffer = new Buffer(buffer); +} + +module.exports = BinaryReader; + +BinaryReader.prototype.readUInt8 = function () { + var value = this._buffer.readUInt8(this._offset); + this._offset += 1; + return value; +}; + +BinaryReader.prototype.readInt8 = function () { + var value = this._buffer.readInt8(this._offset); + this._offset += 1; + return value; +}; + +BinaryReader.prototype.readUInt16 = function () { + var value = this._buffer.readUInt16LE(this._offset); + this._offset += 2; + return value; +}; + +BinaryReader.prototype.readInt16 = function () { + var value = this._buffer.readInt16LE(this._offset); + this._offset += 2; + return value; +}; + +BinaryReader.prototype.readUInt32 = function () { + var value = this._buffer.readUInt32LE(this._offset); + this._offset += 4; + return value; +}; + +BinaryReader.prototype.readInt32 = function () { + var value = this._buffer.readInt32LE(this._offset); + this._offset += 4; + return value; +}; + +BinaryReader.prototype.readFloat = function () { + var value = this._buffer.readFloatLE(this._offset); + this._offset += 4; + return value; +}; + +BinaryReader.prototype.readDouble = function () { + var value = this._buffer.readDoubleLE(this._offset); + this._offset += 8; + return value; +}; + +BinaryReader.prototype.readBytes = function (length) { + var value = this._buffer.slice(this._offset, this._offset + length); + this._offset += length; + return value; +}; + +BinaryReader.prototype.skipBytes = function (length) { + this._offset += length; +}; + +BinaryReader.prototype.readStringUtf8 = function (length) { + if (length == null) length = this._buffer.length - this._offset; + length = Math.max(0, length); + var value = this._buffer.toString('utf8', this._offset, this._offset + length); + this._offset += length; + return value; +}; + +BinaryReader.prototype.readStringUnicode = function (length) { + if (length == null) length = this._buffer.length - this._offset; + length = Math.max(0, length); + var safeLength = length - (length % 2); + safeLength = Math.max(0, safeLength); + var value = this._buffer.toString('ucs2', this._offset, this._offset + safeLength); + this._offset += length; + return value; +}; + +BinaryReader.prototype.readStringZeroUtf8 = function () { + var length = 0; + var terminatorLength = 0; + for (var i = this._offset; i < this._buffer.length; i++) { + if (this._buffer.readUInt8(i) == 0) { + terminatorLength = 1; + break; + } + length++; + } + var value = this.readStringUtf8(length); + this._offset += terminatorLength; + return value; +}; + +BinaryReader.prototype.readStringZeroUnicode = function () { + var length = 0; + var terminatorLength = ((this._buffer.length - this._offset) & 1) != 0 ? 1 : 0; + for (var i = this._offset; i + 1 < this._buffer.length; i += 2) { + if (this._buffer.readUInt16LE(i) == 0) { + terminatorLength = 2; + break; + } + length += 2; + } + var value = this.readStringUnicode(length); + this._offset += terminatorLength; + return value; +}; diff --git a/src/packet/BinaryWriter.js b/src/packet/BinaryWriter.js new file mode 100644 index 000000000..83926b11d --- /dev/null +++ b/src/packet/BinaryWriter.js @@ -0,0 +1,137 @@ +'use strict'; +/* + * Simple BinaryWriter is a minimal tool to write binary stream with unpredictable size. + * Useful for binary serialization. + * + * Copyright (c) 2016 Barbosik + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +function BinaryWriter(size) { + if (!size || size <= 0) { + size = Buffer.poolSize / 2; + } + this._buffer = new Buffer(size); + this._length = 0; +} + +module.exports = BinaryWriter; + +BinaryWriter.prototype.writeUInt8 = function (value) { + checkAlloc(this, 1); + this._buffer[this._length++] = value; +}; + +BinaryWriter.prototype.writeInt8 = function (value) { + checkAlloc(this, 1); + this._buffer[this._length++] = value; +}; + +BinaryWriter.prototype.writeUInt16 = function (value) { + checkAlloc(this, 2); + this._buffer[this._length++] = value; + this._buffer[this._length++] = value >> 8; +}; + +BinaryWriter.prototype.writeInt16 = function (value) { + checkAlloc(this, 2); + this._buffer[this._length++] = value; + this._buffer[this._length++] = value >> 8; +}; + +BinaryWriter.prototype.writeUInt32 = function (value) { + checkAlloc(this, 4); + this._buffer[this._length++] = value; + this._buffer[this._length++] = value >> 8; + this._buffer[this._length++] = value >> 16; + this._buffer[this._length++] = value >> 24; +}; + +BinaryWriter.prototype.writeInt32 = function (value) { + checkAlloc(this, 4); + this._buffer[this._length++] = value; + this._buffer[this._length++] = value >> 8; + this._buffer[this._length++] = value >> 16; + this._buffer[this._length++] = value >> 24; +}; + +BinaryWriter.prototype.writeFloat = function (value) { + checkAlloc(this, 4); + this._buffer.writeFloatLE(value, this._length, true); + this._length += 4; +}; + +BinaryWriter.prototype.writeDouble = function (value) { + checkAlloc(this, 8); + this._buffer.writeDoubleLE(value, this._length, true); + this._length += 8; +}; + +BinaryWriter.prototype.writeBytes = function (data) { + checkAlloc(this, data.length); + data.copy(this._buffer, this._length, 0, data.length); + this._length += data.length; +}; + +BinaryWriter.prototype.writeStringUtf8 = function (value) { + var length = Buffer.byteLength(value, 'utf8') + checkAlloc(this, length); + this._buffer.write(value, this._length, 'utf8'); + this._length += length; +}; + +BinaryWriter.prototype.writeStringUnicode = function (value) { + var length = Buffer.byteLength(value, 'ucs2') + checkAlloc(this, length); + this._buffer.write(value, this._length, 'ucs2'); + this._length += length; +}; + +BinaryWriter.prototype.writeStringZeroUtf8 = function (value) { + this.writeStringUtf8(value); + this.writeUInt8(0); +}; + +BinaryWriter.prototype.writeStringZeroUnicode = function (value) { + this.writeStringUnicode(value); + this.writeUInt16(0); +}; + +BinaryWriter.prototype.getLength = function () { + return this._length; +}; + +BinaryWriter.prototype.reset = function () { + this._length = 0; +}; + +BinaryWriter.prototype.toBuffer = function () { + return Buffer.concat([this._buffer.slice(0, this._length)]); +}; + +function checkAlloc(writer, size) { + var needed = writer._length + size; + if (writer._buffer.length >= needed) + return; + var chunk = Math.max(Buffer.poolSize / 2, 1024); + var chunkCount = (needed / chunk) >>> 0; + if ((needed % chunk) > 0) { + chunkCount += 1; + } + var buffer = new Buffer(chunkCount * chunk); + writer._buffer.copy(buffer, 0, 0, writer._length); + writer._buffer = buffer; +}; diff --git a/src/packet/ChatMessage.js b/src/packet/ChatMessage.js new file mode 100644 index 000000000..b3a5fdaea --- /dev/null +++ b/src/packet/ChatMessage.js @@ -0,0 +1,55 @@ +// Import +var BinaryWriter = require("./BinaryWriter"); +var UserRoleEnum = require("../enum/UserRoleEnum"); + + +function ChatMessage(sender, message) { + this.sender = sender; + this.message = message; +} + +module.exports = ChatMessage; + +ChatMessage.prototype.build = function (protocol) { + var text = this.message; + if (text == null) text = ""; + var name = "SERVER"; + var color = { 'r': 0x9B, 'g': 0x9B, 'b': 0x9B }; + if (this.sender != null) { + name = this.sender.getName(); + if (name == null || name.length == 0) { + if (this.sender.cells.length > 0) + name = "An unnamed cell"; + else + name = "Spectator"; + } + if (this.sender.cells.length > 0) { + color = this.sender.cells[0].getColor(); + } + } + + var writer = new BinaryWriter(); + writer.writeUInt8(0x63); // message id (decimal 99) + + // flags + var flags = 0; + if (this.sender == null) + flags = 0x80; // server message + else if (this.sender.userRole == UserRoleEnum.ADMIN) + flags = 0x40; // admin message + else if (this.sender.userRole == UserRoleEnum.MODER) + flags = 0x20; // moder message + + writer.writeUInt8(flags); + writer.writeUInt8(color.r >> 0); + writer.writeUInt8(color.g >> 0); + writer.writeUInt8(color.b >> 0); + if (protocol <= 5) { + writer.writeStringZeroUnicode(name); + writer.writeStringZeroUnicode(text); + } else { + writer.writeStringZeroUtf8(name); + writer.writeStringZeroUtf8(text); + } + return writer.toBuffer(); +}; diff --git a/src/packet/ClearAll.js b/src/packet/ClearAll.js new file mode 100644 index 000000000..cdc24a1ff --- /dev/null +++ b/src/packet/ClearAll.js @@ -0,0 +1,9 @@ +function ClearAll() { } + +module.exports = ClearAll; + +ClearAll.prototype.build = function (protocol) { + var buffer = new Buffer(1); + buffer.writeUInt8(0x12, 0, true); + return buffer; +}; \ No newline at end of file diff --git a/src/packet/ClearNodes.js b/src/packet/ClearNodes.js deleted file mode 100644 index dece8501f..000000000 --- a/src/packet/ClearNodes.js +++ /dev/null @@ -1,14 +0,0 @@ -function ClearNodes(protocolVersion) { - this.protocolVersion = protocolVersion; -} - -module.exports = ClearNodes; - -ClearNodes.prototype.build = function() { - var buf = new ArrayBuffer(1); - var view = new DataView(buf); - - view.setUint8(0, this.protocolVersion == 5 ? 20 : 18); - - return buf; -}; diff --git a/src/packet/ClearOwned.js b/src/packet/ClearOwned.js new file mode 100644 index 000000000..010e84255 --- /dev/null +++ b/src/packet/ClearOwned.js @@ -0,0 +1,9 @@ +function ClearOwned() { } + +module.exports = ClearOwned; + +ClearOwned.prototype.build = function (protocol) { + var buffer = new Buffer(1); + buffer.writeUInt8(0x14, 0, true); + return buffer; +}; \ No newline at end of file diff --git a/src/packet/DrawLine.js b/src/packet/DrawLine.js index 90d7260b8..9114d9fcc 100644 --- a/src/packet/DrawLine.js +++ b/src/packet/DrawLine.js @@ -1,17 +1,14 @@ -function DrawLine(x, y) { - this.x = x; - this.y = y; -} - -module.exports = DrawLine; - -DrawLine.prototype.build = function() { - var buf = new ArrayBuffer(5); - var view = new DataView(buf); - - view.setUint8(0, 21, true); - view.setUint16(1, this.x, true); - view.setUint16(3, this.y, true); - - return buf; -}; +function DrawLine(x, y) { + this.x = x; + this.y = y; +} + +module.exports = DrawLine; + +DrawLine.prototype.build = function (protocol) { + var buffer = new Buffer(5); + buffer.writeUInt8(0x15, 0); + buffer.writeInt16LE(this.x, 1, true); + buffer.writeInt16LE(this.y, 3, true); + return buffer; +}; diff --git a/src/packet/DynamicBuffer.js b/src/packet/DynamicBuffer.js deleted file mode 100644 index 95aa8f692..000000000 --- a/src/packet/DynamicBuffer.js +++ /dev/null @@ -1,114 +0,0 @@ -function DynamicBuffer(littleEndian) { - this.littleEndian = littleEndian == null ? false : littleEndian; - this.bytes = []; - - // Temp buffers to calculate bytes because I'm too lazy to do it - this.tempArrayBuffer = new ArrayBuffer(8); - this.tempDataView = new DataView(this.tempArrayBuffer); -} - -module.exports = DynamicBuffer; - -// Setters - -DynamicBuffer.prototype.setStringUTF8 = function(value) { - var utf8 = unescape(encodeURIComponent(value)); - for (var i = 0; i < utf8.length; i++) { - this.tempDataView.setUint8(0, utf8.charCodeAt(i), false); - this.moveTemp(1); - } -}; - -DynamicBuffer.prototype.setStringUnicode = function(value) { - for (var i = 0; i < value.length; i++) { - this.tempDataView.setUint16(0, value.charCodeAt(i), false); - this.moveTemp(2); - } -}; - -DynamicBuffer.prototype.setBoolean = function(value) { - this.tempDataView.setUint8(0, value ? 1 : 0, false); - this.moveTemp(1); -}; - -DynamicBuffer.prototype.setUint8 = function(value) { - this.tempDataView.setUint8(0, value, false); - this.moveTemp(1); -}; - -DynamicBuffer.prototype.setInt8 = function(value) { - this.tempDataView.setInt8(0, value, false); - this.moveTemp(1); -}; - -DynamicBuffer.prototype.setUint16 = function(value) { - this.tempDataView.setUint16(0, value, false); - this.moveTemp(2); -}; - -DynamicBuffer.prototype.setInt16 = function(value) { - this.tempDataView.setInt16(0, value, false); - this.moveTemp(2); -}; - -DynamicBuffer.prototype.setUint32 = function(value) { - this.tempDataView.setUint32(0, value, false); - this.moveTemp(4); -}; - -DynamicBuffer.prototype.setInt32 = function(value) { - this.tempDataView.setInt32(0, value, false); - this.moveTemp(4); -}; - -DynamicBuffer.prototype.setUint64 = function(value) { - this.tempDataView.setUint64(0, value, false); - this.moveTemp(8); -}; - -DynamicBuffer.prototype.setInt64 = function(value) { - this.tempDataView.setInt64(0, value, false); - this.moveTemp(8); -}; - -DynamicBuffer.prototype.setFloat32 = function(value) { - this.tempDataView.setFloat32(0, value, false); - this.moveTemp(4); -}; - -DynamicBuffer.prototype.setFloat64 = function(value) { - this.tempDataView.setFloat64(0, value, false); - this.moveTemp(8); -}; - -// Lib - -DynamicBuffer.prototype.moveTemp = function(amount) { - // Moves from temp to calculated bytes and flushes temp - if (this.littleEndian) { - for (var i = amount - 1; i >= 0; i--) { - this.bytes.push(this.tempDataView.getUint8(i)); - } - } else { - for (var i = 0; i < amount; i++) { - this.bytes.push(this.tempDataView.getUint8(i)); - } - } - this.flush(); -}; - -DynamicBuffer.prototype.flush = function() { - // Readies temp buffer for use - this.tempArrayBuffer = new ArrayBuffer(8); - this.tempDataView = new DataView(this.tempArrayBuffer); -}; - -DynamicBuffer.prototype.build = function() { - // Builds fixed array buffer from dynamic byte array - var buf = new ArrayBuffer(this.bytes.length); - var view = new DataView(buf); - for (var i = 0; i < this.bytes.length; i++) { - view.setUint8(i, this.bytes[i], false); - } - return buf; -}; diff --git a/src/packet/ServerStat.js b/src/packet/ServerStat.js new file mode 100644 index 000000000..02370ff46 --- /dev/null +++ b/src/packet/ServerStat.js @@ -0,0 +1,42 @@ +// Import +var BinaryWriter = require("./BinaryWriter"); + +function ServerStat(playerTracker) { + this.playerTracker = playerTracker; +}; + +module.exports = ServerStat; + +ServerStat.prototype.build = function (protocol) { + var gameServer = this.playerTracker.gameServer; + // Get server statistics + var totalPlayers = 0; + var alivePlayers = 0; + var spectPlayers = 0; + for (var i = 0; i < gameServer.clients.length; i++) { + var socket = gameServer.clients[i]; + if (socket == null || !socket.isConnected) + continue; + totalPlayers++; + if (socket.playerTracker.cells.length > 0) + alivePlayers++; + else + spectPlayers++; + } + var obj = { + 'name': gameServer.config.serverName, + 'mode': gameServer.gameMode.name, + 'uptime': process.uptime() >>> 0, + 'update': gameServer.updateTimeAvg.toFixed(3), + 'playersTotal': totalPlayers, + 'playersAlive': alivePlayers, + 'playersSpect': spectPlayers, + 'playersLimit': gameServer.config.serverMaxConnections + }; + var json = JSON.stringify(obj); + // Serialize + var writer = new BinaryWriter(); + writer.writeUInt8(254); // Message Id + writer.writeStringZeroUtf8(json); // JSON + return writer.toBuffer(); +}; diff --git a/src/packet/SetBorder.js b/src/packet/SetBorder.js index 08ed36b39..f9b0c834d 100644 --- a/src/packet/SetBorder.js +++ b/src/packet/SetBorder.js @@ -1,21 +1,40 @@ -function SetBorder(left, right, top, bottom) { - this.left = left; - this.right = right; - this.top = top; - this.bottom = bottom; -} - -module.exports = SetBorder; - -SetBorder.prototype.build = function() { - var buf = new ArrayBuffer(33); - var view = new DataView(buf); - - view.setUint8(0, 64, true); - view.setFloat64(1, this.left, true); - view.setFloat64(9, this.top, true); - view.setFloat64(17, this.right, true); - view.setFloat64(25, this.bottom, true); - - return buf; -}; +// Import +var BinaryWriter = require("./BinaryWriter"); + + +function SetBorder(playerTracker, border, gameType, serverName) { + this.playerTracker = playerTracker; + this.border = border; + this.gameType = gameType; + this.serverName = serverName; +} + +module.exports = SetBorder; + +SetBorder.prototype.build = function (protocol) { + var scrambleX = this.playerTracker.scrambleX; + var scrambleY = this.playerTracker.scrambleY; + if (this.gameType == null) { + var buffer = new Buffer(33); + buffer.writeUInt8(0x40, 0, true); + buffer.writeDoubleLE(this.border.minx + scrambleX, 1, true); + buffer.writeDoubleLE(this.border.miny + scrambleY, 9, true); + buffer.writeDoubleLE(this.border.maxx + scrambleX, 17, true); + buffer.writeDoubleLE(this.border.maxy + scrambleY, 25, true); + return buffer; + } + var writer = new BinaryWriter(); + writer.writeUInt8(0x40); // Packet ID + writer.writeDouble(this.border.minx + scrambleX); + writer.writeDouble(this.border.miny + scrambleY); + writer.writeDouble(this.border.maxx + scrambleX); + writer.writeDouble(this.border.maxy + scrambleY); + writer.writeUInt32(this.gameType >> 0); + var name = this.serverName; + if (name == null) name = ""; + if (protocol < 6) + writer.writeStringZeroUnicode(name); + else + writer.writeStringZeroUtf8(name); + return writer.toBuffer(); +}; diff --git a/src/packet/UpdateLeaderboard.js b/src/packet/UpdateLeaderboard.js index 60982ea90..509992069 100644 --- a/src/packet/UpdateLeaderboard.js +++ b/src/packet/UpdateLeaderboard.js @@ -1,66 +1,122 @@ -var DynamicBuffer = require('./DynamicBuffer'); - -function UpdateLeaderboard(leaderboard, packetLB, protocolVersion, sendingUser) { - this.leaderboard = leaderboard; - this.packetLB = packetLB; - this.protocolVersion = protocolVersion; - this.sendingUser = sendingUser; -} - -module.exports = UpdateLeaderboard; - -UpdateLeaderboard.prototype.build = function() { - var buffer = new DynamicBuffer(true); - - switch (this.packetLB) { - case 48: - // Custom text list - buffer.setUint8(48); // Packet ID - buffer.setUint32(this.leaderboard.length); // String amount - - for (var i = 0; i < this.leaderboard.length; i++) { - if (this.protocolVersion != 5) { - buffer.setStringUTF8( // UTF-8 string - this.leaderboard[i] ? this.leaderboard[i] : ""); - buffer.setUint8(0); // UTF-8 null terminator - } else { - buffer.setStringUnicode( // Unicode string - this.leaderboard[i] ? this.leaderboard[i] : ""); - buffer.setUint16(0); // Unicode null terminator - } - } - break; - case 49: - // FFA leaderboard list - buffer.setUint8(49); // Packet ID - buffer.setUint32(this.leaderboard.length); // Player amount - for (var i = 0; i < this.leaderboard.length; i++) { - var player = this.leaderboard[i]; - var name = player.getName(); - name = name ? name : ""; - if (this.protocolVersion != 5) { - var isMe = player.pID == this.sendingUser ? 1 : 0; - buffer.setUint32(isMe); // If to display red color text - buffer.setStringUTF8(name); // UTF-8 string - buffer.setUint8(0); // UTF-8 null terminator - } else { - if (player.cells[0]) - buffer.setUint32(player.cells[0].nodeId); // First cell node ID - else buffer.setUint32(0); // In case of error - buffer.setStringUnicode(name); // Unicode string - buffer.setUint16(0); // Unicode null terminator - } - } - break; - case 50: - // Pie chart - buffer.setUint8(50); // Packet ID - buffer.setUint32(this.leaderboard.length); // Color amount - for (var i = 0; i < this.leaderboard.length; i++) { - buffer.setFloat32(this.leaderboard[i]); // A color's size - } - break; - } - - return buffer.build(); -}; +// Import +var BinaryWriter = require("./BinaryWriter"); + + +function UpdateLeaderboard(playerTracker, leaderboard, leaderboardType) { + this.playerTracker = playerTracker; + this.leaderboard = leaderboard; + this.leaderboardType = leaderboardType; +} + +module.exports = UpdateLeaderboard; + +UpdateLeaderboard.prototype.build = function (protocol) { + switch (this.leaderboardType) { + case 48: + // UserText + return this.buildUserText(protocol); + case 49: + // FFA + if (protocol < 6) + return this.buildFfa5(); + return this.buildFfa6(); + case 50: + // Team + return this.buildTeam(); + default: + return null; + } +} + +// UserText +UpdateLeaderboard.prototype.buildUserText = function (protocol) { + var writer = new BinaryWriter(); + writer.writeUInt8(0x31); // Packet ID + writer.writeUInt32(this.leaderboard.length >>> 0); // Number of elements + for (var i = 0; i < this.leaderboard.length; i++) { + var item = this.leaderboard[i]; + if (item == null) return null; // bad leaderboardm just don't send it + + var name = item; + name = name ? name : ""; + var id = 0; + + writer.writeUInt32(id >> 0); // isMe flag/cell ID + if (protocol <= 5) + writer.writeStringZeroUnicode(name); + else + writer.writeStringZeroUtf8(name); + } + return writer.toBuffer(); +}; + +// FFA protocol 5 +UpdateLeaderboard.prototype.buildFfa5 = function () { + var player = this.playerTracker; + if (player.spectate && player.spectateTarget != null) { + player = player.spectateTarget; + } + var writer = new BinaryWriter(); + writer.writeUInt8(0x31); // Packet ID + writer.writeUInt32(this.leaderboard.length >>> 0); // Number of elements + for (var i = 0; i < this.leaderboard.length; i++) { + var item = this.leaderboard[i]; + if (item == null) return null; // bad leaderboardm just don't send it + + var name = item.getNameUnicode(); + var id = 0; + if (item == player && item.cells.length > 0) { + id = item.cells[0].nodeId ^ this.playerTracker.scrambleId; + } + + writer.writeUInt32(id >>> 0); // Player cell Id + if (name != null) + writer.writeBytes(name); + else + writer.writeUInt16(0); + } + return writer.toBuffer(); +}; + +// FFA protocol 6 +UpdateLeaderboard.prototype.buildFfa6 = function () { + var player = this.playerTracker; + if (player.spectate && player.spectateTarget != null) { + player = player.spectateTarget; + } + var writer = new BinaryWriter(); + writer.writeUInt8(0x31); // Packet ID + writer.writeUInt32(this.leaderboard.length >>> 0); // Number of elements + for (var i = 0; i < this.leaderboard.length; i++) { + var item = this.leaderboard[i]; + if (item == null) return null; // bad leaderboardm just don't send it + + var name = item.getNameUtf8(); + var id = item == player ? 1 : 0; + + writer.writeUInt32(id >>> 0); // isMe flag + if (name != null) + writer.writeBytes(name); + else + writer.writeUInt8(0); + } + return writer.toBuffer(); +}; + +// Team +UpdateLeaderboard.prototype.buildTeam = function () { + var writer = new BinaryWriter(); + writer.writeUInt8(0x32); // Packet ID + writer.writeUInt32(this.leaderboard.length >>> 0); // Number of elements + for (var i = 0; i < this.leaderboard.length; i++) { + var value = this.leaderboard[i]; + if (value == null) return null; // bad leaderboardm just don't send it + + if (isNaN(value)) value = 0; + value = value < 0 ? 0 : value; + value = value > 1 ? 1 : value; + + writer.writeFloat(value); // isMe flag (previously cell ID) + } + return writer.toBuffer(); +}; \ No newline at end of file diff --git a/src/packet/UpdateNodes.js b/src/packet/UpdateNodes.js index 4c10804a0..1931ccd6d 100644 --- a/src/packet/UpdateNodes.js +++ b/src/packet/UpdateNodes.js @@ -1,90 +1,302 @@ -var DynamicBuffer = require('./DynamicBuffer'); - -function UpdateNodes(destroyQueue, nodes, nonVisibleNodes, scrambleX, scrambleY, protocolVersion) { - this.destroyQueue = destroyQueue; - this.nodes = nodes; - this.nonVisibleNodes = nonVisibleNodes; - this.scrambleX = scrambleX; - this.scrambleY = scrambleY; - this.protocolVersion = protocolVersion; -} - -module.exports = UpdateNodes; - -UpdateNodes.prototype.build = function() { - var buffer = new DynamicBuffer(true); // Little endian included - - buffer.setUint8(16); // Packet ID - - // Check for invalid nodes in any case - var deadCells = []; - for (var i = 0; i < this.destroyQueue.length; i++) { - deadCells.push(this.destroyQueue[i]); - } - - buffer.setUint16(deadCells.length); // Eat actions length - for (var i = 0; i < deadCells.length; i++) { - var node = deadCells[i]; - var id = 0; - if (node.getKiller()) id = node.getKiller().nodeId; - buffer.setUint32(id); // Eaten ID - buffer.setUint32(node.nodeId); // Eater ID - } - - for (var i = 0; i < this.nodes.length; i++) { // Update nodes - var node = this.nodes[i]; - - if (node.nodeId == 0) continue; // Error! - buffer.setUint32(node.nodeId); // Node ID - buffer.setInt32(node.position.x + this.scrambleX); // Node's X pos - buffer.setInt32(node.position.y + this.scrambleY); // Node's Y pos - buffer.setUint16(node.getSize()); // Node size - - var flags = 0; - - if (this.protocolVersion != 5) { - // Flags - if (node.getName() != null && node.getName() != "") flags += 8; - flags += 2; - if (node.spiked) flags += 1; - - buffer.setUint8(flags); // Node's update flags - buffer.setUint8(node.color.r); // Node's R color - buffer.setUint8(node.color.g); // Node's G color - buffer.setUint8(node.color.b); // Node's B color - if (node.getName() != null && node.getName() != "") { - buffer.setStringUTF8(node.getName()); // Node's name - buffer.setUint8(0); // Node name terminator - } - } else { - // Flags - if (node.spiked) flags += 1; - - buffer.setUint8(node.color.r); // Node's R color - buffer.setUint8(node.color.g); // Node's G color - buffer.setUint8(node.color.b); // Node's B color - buffer.setUint8(flags); // Node's update flags - if (node.getName() != null && node.getName() != "") { - buffer.setStringUnicode(node.getName()); // Node's name - } - buffer.setUint8(0); // Node name terminator - } - } - buffer.setUint32(0); // Update nodes end - - // Add non-visible cells to the "dead cells" list - for (var i = 0; i < this.nonVisibleNodes.length; i++) { - deadCells.push(this.nonVisibleNodes[i]); - } - - if (this.protocolVersion != 5) { - buffer.setUint16(deadCells.length); // Remove actions length - } else { - buffer.setUint32(deadCells.length); // Remove actions length - } - for (var i = 0; i < deadCells.length; i++) { - buffer.setUint32(deadCells[i].nodeId); // Removing node's ID - } - - return buffer.build(); -}; +// Import +var BinaryWriter = require("./BinaryWriter"); +var Logger = require('../modules/Logger'); + +var sharedWriter = new BinaryWriter(128*1024); // for about 25000 cells per client + +function UpdateNodes(playerTracker, addNodes, updNodes, eatNodes, delNodes) { + this.playerTracker = playerTracker; + this.addNodes = addNodes; + this.updNodes = updNodes; + this.eatNodes = eatNodes; + this.delNodes = delNodes; +} + +module.exports = UpdateNodes; + +UpdateNodes.prototype.build = function (protocol) { + if (!protocol) return null; + + var writer = sharedWriter; + writer.reset(); + writer.writeUInt8(0x10); // Packet ID + this.writeEatItems(writer); + + if (protocol < 5) this.writeUpdateItems4(writer); + else if (protocol == 5) this.writeUpdateItems5(writer); + else this.writeUpdateItems6(writer); + + this.writeRemoveItems(writer, protocol); + return writer.toBuffer(); +}; + +// protocol 4 +UpdateNodes.prototype.writeUpdateItems4 = function (writer) { + var scrambleX = this.playerTracker.scrambleX; + var scrambleY = this.playerTracker.scrambleY; + var scrambleId = this.playerTracker.scrambleId; + + for (var i = 0; i < this.updNodes.length; i++) { + var node = this.updNodes[i]; + if (node.nodeId == 0) + continue; + var cellX = node.position.x + scrambleX; + var cellY = node.position.y + scrambleY; + + // Write update record + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + writer.writeInt16(cellX >> 0); // Coordinate X + writer.writeInt16(cellY >> 0); // Coordinate Y + writer.writeUInt16(node.getSize() >>> 0); // Cell Size (not to be confused with mass, because mass = size*size/100) + var color = node.getColor(); + writer.writeUInt8(color.r >>> 0); // Color R + writer.writeUInt8(color.g >>> 0); // Color G + writer.writeUInt8(color.b >>> 0); // Color B + + var flags = 0; + if (node.isSpiked) + flags |= 0x01; // isVirus + if (node.isAgitated) + flags |= 0x10; // isAgitated + if (node.cellType == 3) + flags |= 0x20; // isEjected + writer.writeUInt8(flags >>> 0); // Flags + + writer.writeUInt16(0); // Name + } + for (var i = 0; i < this.addNodes.length; i++) { + var node = this.addNodes[i]; + if (node.nodeId == 0) + continue; + var cellX = node.position.x + scrambleX; + var cellY = node.position.y + scrambleY; + var cellName = null; + if (node.owner) { + cellName = node.owner.getNameUnicode(); + } + + // Write update record + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + writer.writeInt16(cellX >> 0); // Coordinate X + writer.writeInt16(cellY >> 0); // Coordinate Y + writer.writeUInt16(node.getSize() >>> 0); // Cell Size (not to be confused with mass, because mass = size*size/100) + var color = node.getColor(); + writer.writeUInt8(color.r >>> 0); // Color R + writer.writeUInt8(color.g >>> 0); // Color G + writer.writeUInt8(color.b >>> 0); // Color B + + var flags = 0; + if (node.isSpiked) + flags |= 0x01; // isVirus + if (node.isAgitated) + flags |= 0x10; // isAgitated + if (node.cellType == 3) + flags |= 0x20; // isEjected + writer.writeUInt8(flags >>> 0); // Flags + + if (cellName != null) + writer.writeBytes(cellName); // Name + else + writer.writeUInt16(0); // Name + } + writer.writeUInt32(0); // Cell Update record terminator +}; + +// protocol 5 +UpdateNodes.prototype.writeUpdateItems5 = function (writer) { + var scrambleX = this.playerTracker.scrambleX; + var scrambleY = this.playerTracker.scrambleY; + var scrambleId = this.playerTracker.scrambleId; + + for (var i = 0; i < this.updNodes.length; i++) { + var node = this.updNodes[i]; + if (node.nodeId == 0) + continue; + var cellX = node.position.x + scrambleX; + var cellY = node.position.y + scrambleY; + + // Write update record + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + writer.writeInt32(cellX >> 0); // Coordinate X + writer.writeInt32(cellY >> 0); // Coordinate Y + writer.writeUInt16(node.getSize() >>> 0); // Cell Size (not to be confused with mass, because mass = size*size/100) + var color = node.getColor(); + writer.writeUInt8(color.r >>> 0); // Color R + writer.writeUInt8(color.g >>> 0); // Color G + writer.writeUInt8(color.b >>> 0); // Color B + + var flags = 0; + if (node.isSpiked) + flags |= 0x01; // isVirus + if (node.isAgitated) + flags |= 0x10; // isAgitated + if (node.cellType == 3) + flags |= 0x20; // isEjected + writer.writeUInt8(flags >>> 0); // Flags + + writer.writeUInt16(0); // Cell Name + } + for (var i = 0; i < this.addNodes.length; i++) { + var node = this.addNodes[i]; + if (node.nodeId == 0) + continue; + + var cellX = node.position.x + scrambleX; + var cellY = node.position.y + scrambleY; + var skinName = null; + var cellName = null; + if (node.owner) { + skinName = node.owner.getSkinUtf8(); + cellName = node.owner.getNameUnicode(); + } + + // Write update record + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + writer.writeInt32(cellX >> 0); // Coordinate X + writer.writeInt32(cellY >> 0); // Coordinate Y + writer.writeUInt16(node.getSize() >>> 0); // Cell Size (not to be confused with mass, because mass = size*size/100) + var color = node.getColor(); + writer.writeUInt8(color.r >>> 0); // Color R + writer.writeUInt8(color.g >>> 0); // Color G + writer.writeUInt8(color.b >>> 0); // Color B + + var flags = 0; + if (node.isSpiked) + flags |= 0x01; // isVirus + if (skinName != null) + flags |= 0x04; // isSkinPresent + if (node.isAgitated) + flags |= 0x10; // isAgitated + if (node.cellType == 3) + flags |= 0x20; // isEjected + writer.writeUInt8(flags >>> 0); // Flags + + if (flags & 0x04) + writer.writeBytes(skinName); // Skin Name in UTF8 + + if (cellName != null) + writer.writeBytes(cellName); // Name + else + writer.writeUInt16(0); // Name + } + writer.writeUInt32(0 >> 0); // Cell Update record terminator +}; + +// protocol 6 +UpdateNodes.prototype.writeUpdateItems6 = function (writer) { + var scrambleX = this.playerTracker.scrambleX; + var scrambleY = this.playerTracker.scrambleY; + var scrambleId = this.playerTracker.scrambleId; + for (var i = 0; i < this.updNodes.length; i++) { + var node = this.updNodes[i]; + if (node.nodeId == 0) + continue; + + var cellX = node.position.x + scrambleX; + var cellY = node.position.y + scrambleY; + + // Write update record + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + writer.writeInt32(cellX >> 0); // Coordinate X + writer.writeInt32(cellY >> 0); // Coordinate Y + writer.writeUInt16(node.getSize() >>> 0); // Cell Size (not to be confused with mass, because mass = size*size/100) + + var flags = 0; + if (node.isSpiked) + flags |= 0x01; // isVirus + if (node.cellType == 0) + flags |= 0x02; // isColorPresent (for players only) + if (node.isAgitated) + flags |= 0x10; // isAgitated + if (node.cellType == 3) + flags |= 0x20; // isEjected + writer.writeUInt8(flags >>> 0); // Flags + + if (flags & 0x02) { + var color = node.getColor(); + writer.writeUInt8(color.r >>> 0); // Color R + writer.writeUInt8(color.g >>> 0); // Color G + writer.writeUInt8(color.b >>> 0); // Color B + } + } + for (var i = 0; i < this.addNodes.length; i++) { + var node = this.addNodes[i]; + if (node.nodeId == 0) + continue; + + var cellX = node.position.x + scrambleX; + var cellY = node.position.y + scrambleY; + var skinName = null; + var cellName = null; + if (node.owner) { + skinName = node.owner.getSkinUtf8(); + cellName = node.owner.getNameUtf8(); + } + + // Write update record + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + writer.writeInt32(cellX >> 0); // Coordinate X + writer.writeInt32(cellY >> 0); // Coordinate Y + writer.writeUInt16(node.getSize() >>> 0); // Cell Size (not to be confused with mass, because mass = size*size/100) + + var flags = 0; + if (node.isSpiked) + flags |= 0x01; // isVirus + if (true) + flags |= 0x02; // isColorPresent (always for added) + if (skinName != null) + flags |= 0x04; // isSkinPresent + if (cellName != null) + flags |= 0x08; // isNamePresent + if (node.isAgitated) + flags |= 0x10; // isAgitated + if (node.cellType == 3) + flags |= 0x20; // isEjected + writer.writeUInt8(flags >>> 0); // Flags + + if (flags & 0x02) { + var color = node.getColor(); + writer.writeUInt8(color.r >>> 0); // Color R + writer.writeUInt8(color.g >>> 0); // Color G + writer.writeUInt8(color.b >>> 0); // Color B + } + if (flags & 0x04) + writer.writeBytes(skinName); // Skin Name in UTF8 + if (flags & 0x08) + writer.writeBytes(cellName); // Cell Name in UTF8 + } + writer.writeUInt32(0); // Cell Update record terminator +}; + +UpdateNodes.prototype.writeEatItems = function (writer) { + var scrambleId = this.playerTracker.scrambleId; + + writer.writeUInt16(this.eatNodes.length >>> 0); // EatRecordCount + for (var i = 0; i < this.eatNodes.length; i++) { + var node = this.eatNodes[i]; + var hunterId = 0; + if (node.getKiller()) { + hunterId = node.getKiller().nodeId; + } + writer.writeUInt32((hunterId ^ scrambleId) >>> 0); // Hunter ID + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Prey ID + } +}; + +UpdateNodes.prototype.writeRemoveItems = function (writer, protocol) { + var scrambleId = this.playerTracker.scrambleId; + + var length = this.eatNodes.length + this.delNodes.length; + if (protocol < 6) + writer.writeUInt32(length >>> 0); // RemoveRecordCount + else + writer.writeUInt16(length >>> 0); // RemoveRecordCount + for (var i = 0; i < this.eatNodes.length; i++) { + var node = this.eatNodes[i]; + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + } + for (var i = 0; i < this.delNodes.length; i++) { + var node = this.delNodes[i]; + writer.writeUInt32((node.nodeId ^ scrambleId) >>> 0); // Cell ID + } +}; diff --git a/src/packet/UpdatePosition.js b/src/packet/UpdatePosition.js index e7b49efa0..c92a1110a 100644 --- a/src/packet/UpdatePosition.js +++ b/src/packet/UpdatePosition.js @@ -1,19 +1,22 @@ -function UpdatePosition(x, y, size) { - this.x = x; - this.y = y; - this.size = size; -} - -module.exports = UpdatePosition; - -UpdatePosition.prototype.build = function() { - var buf = new ArrayBuffer(13); - var view = new DataView(buf); - - view.setUint8(0, 17, true); - view.setFloat32(1, this.x, true); - view.setFloat32(5, this.y, true); - view.setFloat32(9, this.size, true); - - return buf; -}; +function UpdatePosition(playerTracker, x, y, scale) { + this.playerTracker = playerTracker, + this.x = x; + this.y = y; + this.scale = scale; +} + +module.exports = UpdatePosition; + +UpdatePosition.prototype.build = function (protocol) { + var buffer = new Buffer(13); + var offset = 0; + buffer.writeUInt8(0x11, offset, true); + offset += 1; + buffer.writeFloatLE(this.x + this.playerTracker.scrambleX, offset, true); + offset += 4; + buffer.writeFloatLE(this.y + this.playerTracker.scrambleY, offset, true); + offset += 4; + buffer.writeFloatLE(this.scale, offset, true); + offset += 4; + return buffer; +}; diff --git a/src/packet/index.js b/src/packet/index.js index 9e44c4627..5f4003785 100644 --- a/src/packet/index.js +++ b/src/packet/index.js @@ -1,7 +1,12 @@ -module.exports = { +module.exports = { + BinaryWriter: require('./BinaryWriter'), + BinaryReader: require('./BinaryReader'), + ChatMessage: require('./ChatMessage'), AddNode: require('./AddNode'), - ClearNodes: require('./ClearNodes'), + ClearAll: require('./ClearAll'), + ClearOwned: require('./ClearOwned'), UpdatePosition: require('./UpdatePosition'), + ServerStat: require('./ServerStat'), SetBorder: require('./SetBorder'), UpdateNodes: require('./UpdateNodes'), UpdateLeaderboard: require('./UpdateLeaderboard'), diff --git a/src/userRoles.json b/src/userRoles.json new file mode 100644 index 000000000..ccbd07562 --- /dev/null +++ b/src/userRoles.json @@ -0,0 +1,20 @@ +[ + { + "ip":"127.0.0.1", + "password":"", + "role":"ADMIN", + "name":"Local Administrator" + }, + { + "ip":"127.0.0.1", + "password":"", + "role":"MODER", + "name":"Local Moderator" + }, + { + "ip":"127.0.0.1", + "password":"", + "role":"USER", + "name":"Local User" + } +] diff --git a/ssl/readme.txt b/ssl/readme.txt new file mode 100644 index 000000000..ecc03e9f2 --- /dev/null +++ b/ssl/readme.txt @@ -0,0 +1,7 @@ +Place private key and certificate files here: +key.pem - your private key +cert.pem - your certificate + +You can create it with openssl: + +openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 100 -nodes \ No newline at end of file