├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── cli.js ├── package-lock.json ├── package.json └── src ├── api ├── index.js └── index.test.js ├── exceptions.js ├── game └── index.js ├── index.js └── util.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .gitattributes 4 | 5 | LICENSE 6 | README.md -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-base', 'prettier'], 3 | env: { 4 | node: true 5 | }, 6 | plugins: ['jest', 'prettier'], 7 | rules: { 8 | 'prettier/prettier': 'warn', 9 | 10 | 'no-await-in-loop': 'off', 11 | 12 | 'jest/no-disabled-tests': 'warn', 13 | 'jest/no-focused-tests': 'error', 14 | 'jest/no-identical-title': 'error', 15 | 'jest/valid-expect': 'error', 16 | 17 | // Rules below are all added to remove conflicts with prettier. DO NOT REMOVE 18 | 'arrow-parens': 'off', 19 | }, 20 | overrides: [ 21 | { 22 | files: ['**/*.test.js'], 23 | env: { 24 | jest: true 25 | } 26 | } 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.md text 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *token.txt 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | install: 5 | - npm i 6 | script: 7 | - npm run lint 8 | - npm run test/coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN npm install 8 | 9 | ENTRYPOINT ["node", "bin/cli.js"] 10 | 11 | CMD ["--help"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Gabites 4 | 5 | https://github.com/South-Paw/salien-script-js 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # salien-script-js 2 | 3 | 👽 A easy to install, run and update Node.js script for the Steam salien mini-game. 4 | 5 | > A Node.js implementation of https://github.com/SteamDatabase/SalienCheat by [xPaw](https://github.com/xPaw) with additional features! 6 | 7 | [![npm](https://img.shields.io/npm/v/salien-script-js.svg)](https://www.npmjs.com/package/salien-script-js) 8 | [![CI Status](https://img.shields.io/travis/South-Paw/salien-script-js.svg)](https://travis-ci.org/South-Paw/salien-script-js) 9 | [![Coveralls Status](https://img.shields.io/coveralls/github/South-Paw/salien-script-js.svg)](https://coveralls.io/github/South-Paw/salien-script-js) 10 | [![Dependencies](https://david-dm.org/South-Paw/salien-script-js.svg)](https://david-dm.org/South-Paw/salien-script-js) 11 | [![Dev Dependencies](https://david-dm.org/South-Paw/salien-script-js/dev-status.svg)](https://david-dm.org/South-Paw/salien-script-js?type=dev) 12 | 13 | --- 14 | 15 | ## 🌈 Features 16 | 17 | * 🎉 [Easy to install, run and update](#️-how-to-use-this) 18 | * ✉️ [Update checker and log notifications](#-how-to-update-the-script) 19 | * 👽 Same logic as the [PHP version](https://github.com/SteamDatabase/SalienCheat) (we almost have parity) 20 | * 👌 [Pick your own steam group](#-represent-your-steam-group-optional) 21 | * 👥 [Works well with multiple tokens/scripts](#-multiple-tokensscripts) 22 | * 👀 [Name your running scripts](#-multiple-tokensscripts) 23 | * 🐳 [Docker support](#advanced--running-as-a-docker-container) 24 | * 📦 [npm package export](#advanced--usage-as-an-npm-package) 25 | 26 | > Note: We'll try our best to keep this version up to date with the PHP and other versions! Suggestions welcome. 27 | 28 | --- 29 | 30 | ## 🕹️ How to use this 31 | 32 | 1. Install [Node.js](https://nodejs.org/en/). (Version 10 and above) 33 | 2. Log into [Steam](http://store.steampowered.com/) in your browser. 34 | 3. Open the following URL: . You should be able to find the bit that looks like `"token":"xxxxxxxx"`. Copy whatever is inside the second quotes, (e.g. `xxxxxxxx`). 35 | 4. Open PowerShell on Windows. (Tip: Start > Run > type `powershell.exe` > Enter) 36 | 5. Run `npm install -g salien-script-js` to install this project. 37 | 6. Run the script by typing `salien-script-js --token xxxxxxxx` where `xxxxxxxx` is your token from step 3. 38 | 39 | > ### Remeber to drop us a ⭐ star on the project if you appreciate this script! 40 | 41 | ## 😍 How to update the script 42 | 43 | 1. Close/cancel any running script windows 44 | 2. Open PowerShell on Windows. 45 | 3. Run `npm i -g salien-script-js` 46 | 4. Re-run your scripts using the same command 47 | 48 | Easy right? 49 | 50 | --- 51 | 52 | ### 👌 Represent your Steam Group (Optional) 53 | 54 | If you'd like to represent a specific steam group, simply pass the `--group` option with the ID of the group. 55 | 56 | ```sh-session 57 | salien-script-js --token xxxxxxxx --group 123456789 58 | ``` 59 | 60 | You can get your group id by going to https://steamcommunity.com/groups/YOUR_GROUP_NAME_HERE/memberslistxml/?xml=1 and replacing `YOUR_GROUP_NAME_HERE` with the group name shown at the end of your groups url. 61 | 62 | **You must be a member of a group to represent that group!** 63 | 64 | If you'd like to team up with an established larger group please consider using either: 65 | 66 | * [/r/saliens](https://steamcommunity.com/groups/summersaliens) id: `103582791462557324` 67 | * [SteamDB](https://steamcommunity.com/groups/steamdb) id: `103582791434298690` 68 | * [100Pals](https://steamcommunity.com/groups/100pals) id: `103582791454524084` 69 | 70 | ### 👥 Multiple tokens/scripts 71 | 72 | Simply open another PowerShell window and run `salien-script-js --token yyyyyyyy --name "name of this script"` where `yyyyyyyy` is your other accounts token and `name of this script` if what you'd like to see in the log outputs. 73 | 74 | --- 75 | 76 | ### Advanced: CLI Arguments 77 | 78 | ``` 79 | Usage: 80 | salien-script-js [options] 81 | 82 | Options: 83 | --token, -t Your Saliens game token. 84 | --group, -g (Optional) The ID of a steam group you'd like to represent. 85 | --name, -n (Optional) The name to display on this instance of the script. 86 | --logRequests, -l (Optional) Set to true if you'd like to show Steam API requests in the logs. 87 | ``` 88 | 89 | ## Advanced: 📦 Usage as an npm package 90 | 91 | ```js 92 | const SalienScript = require('salien-script-js'); 93 | 94 | const config = { 95 | token: '', // Your token from https://steamcommunity.com/saliengame/gettoken 96 | clan: '', // (optional) Clan id from https://steamcommunity.com/groups/YOUR_GROUP_NAME_HERE/memberslistxml/?xml=1 97 | name: '', // (optional) Name of this instance for logging 98 | }; 99 | 100 | const salien = new SalienScript(config); 101 | 102 | salien.init(); 103 | ``` 104 | 105 | ## Advanced: 🐳 Running as a Docker container 106 | 107 | The provided Dockerfile allows you to build this repository as a Docker container. To do that, clone the following repo and run the following commands. 108 | 109 | ```bash 110 | # builds an image of the repo 111 | $ docker build -t salien-script-js . 112 | 113 | # sets up a container based on said image in "detached" mode 114 | $ docker run -d --name salien-script-js salien-script-js [options] 115 | ``` 116 | 117 | You can also set up continuous deployment through Docker Hub. [Read the following comment](https://github.com/South-Paw/salien-script-js/pull/11#issuecomment-399747215) for a guide. 118 | 119 | --- 120 | 121 | ## 👨‍💻 Contributing and Development 122 | 123 | Want to help out? Awesome! 👍 124 | 125 | Pull the repo and you can run the script with `node cli.js -t TOKEN`. 126 | 127 | PRs, suggestions, fixes and improvements all welcome. 128 | 129 | --- 130 | 131 | ## License 132 | 133 | This project is licensed under [MIT](https://github.com/South-Paw/salien-script-js/blob/master/LICENSE) 134 | 135 | ``` 136 | MIT License 137 | 138 | Copyright (c) 2018 Alex Gabites 139 | 140 | https://github.com/South-Paw/salien-script-js 141 | 142 | Permission is hereby granted, free of charge, to any person obtaining a copy 143 | of this software and associated documentation files (the "Software"), to deal 144 | in the Software without restriction, including without limitation the rights 145 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 146 | copies of the Software, and to permit persons to whom the Software is 147 | furnished to do so, subject to the following conditions: 148 | 149 | The above copyright notice and this permission notice shall be included in all 150 | copies or substantial portions of the Software. 151 | 152 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 153 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 154 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 155 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 156 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 157 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 158 | SOFTWARE. 159 | ``` 160 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const meow = require('meow'); 4 | const SalienScript = require('../src/index.js'); 5 | 6 | const cliOptions = { 7 | flags: { 8 | token: { 9 | type: 'string', 10 | alias: 't', 11 | }, 12 | group: { 13 | type: 'string', 14 | alias: 'g', 15 | }, 16 | name: { 17 | type: 'string', 18 | alias: 'n', 19 | }, 20 | logRequests: { 21 | type: 'boolean', 22 | alias: 'l', 23 | }, 24 | }, 25 | }; 26 | 27 | const cli = meow( 28 | ` 29 | Usage: 30 | salien-script-js [options] 31 | 32 | Options: 33 | --token, -t Your Saliens game token. 34 | --group, -g (Optional) The ID of a steam group you'd like to represent. 35 | --name, -n (Optional) The name to display on this instance of the script. 36 | --logRequests, -l (Optional) Set to true if you'd like to show Steam API requests in the logs. 37 | `, 38 | cliOptions, 39 | ); 40 | 41 | if (cli.flags.token) { 42 | const salien = new SalienScript({ 43 | token: cli.flags.token, 44 | clan: cli.flags.group, 45 | name: cli.flags.name, 46 | logRequests: cli.flags.logRequests, 47 | }); 48 | 49 | salien.init(); 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salien-script-js", 3 | "version": "0.0.17", 4 | "description": "A easy to install, run and update Node.js script for the Steam salien mini-game.", 5 | "keywords": [ 6 | "salien", 7 | "steam", 8 | "steam-sale", 9 | "sale", 10 | "game", 11 | "script", 12 | "steam-salien", 13 | "salien-cheat", 14 | "salien-script", 15 | "saliens" 16 | ], 17 | "homepage": "https://github.com/South-Paw/salien-script-js", 18 | "bugs": "https://github.com/South-Paw/salien-script-js/issues", 19 | "license": "MIT", 20 | "contributors": [ 21 | { 22 | "name": "Alex Gabites", 23 | "email": "hello@southpaw.co.nz", 24 | "url": "http://southpaw.co.nz/" 25 | }, 26 | { 27 | "name": "Resi Respati", 28 | "email": "resir014@gmail.com", 29 | "url": "https://resir014.xyz/" 30 | } 31 | ], 32 | "files": [ 33 | "bin", 34 | "src" 35 | ], 36 | "bin": { 37 | "salien-script-js": "./bin/cli.js" 38 | }, 39 | "main": "src/index.js", 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/South-Paw/salien-script-js.git" 43 | }, 44 | "scripts": { 45 | "lint": "eslint ./src", 46 | "prepublishOnly": "npm run lint", 47 | "prettier": "prettier --write \"src/**/*.js\"", 48 | "test": "jest", 49 | "test/coverage": "jest --coverage" 50 | }, 51 | "dependencies": { 52 | "chalk": "^2.4.1", 53 | "dateformat": "^3.0.3", 54 | "delay": "^3.0.0", 55 | "meow": "^5.0.0", 56 | "node-fetch": "^2.1.2", 57 | "update-check": "^1.5.2" 58 | }, 59 | "devDependencies": { 60 | "coveralls": "^3.0.1", 61 | "eslint": "^5.0.0", 62 | "eslint-config-airbnb-base": "^13.0.0", 63 | "eslint-config-prettier": "^2.9.0", 64 | "eslint-plugin-import": "^2.12.0", 65 | "eslint-plugin-jest": "^21.17.0", 66 | "eslint-plugin-prettier": "^2.6.0", 67 | "jest": "^23.1.0", 68 | "prettier": "^1.13.5" 69 | }, 70 | "engines": { 71 | "node": ">=10.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const delay = require('delay'); 3 | const fetch = require('node-fetch'); 4 | 5 | const { SalienScriptException } = require('../exceptions'); 6 | 7 | const MAX_RETRIES = 3; 8 | const RETRY_DELAY_MS = 3000; 9 | 10 | const doFetch = async (method, params, requestOptions = {}, logger, isSilentRequest, maxRetries, retryDelayMs) => { 11 | let url = `https://community.steam-api.com/${method}`; 12 | 13 | if (params) { 14 | url += '/?'; 15 | 16 | params.forEach(param => { 17 | url += `${param}&`; 18 | }); 19 | 20 | url = url.substring(0, url.length - 1); 21 | } 22 | 23 | const options = { 24 | headers: { 25 | 'User-Agent': 'salien-script-js (https://github.com/South-Paw/salien-script-js)', 26 | }, 27 | ...requestOptions, 28 | }; 29 | 30 | let request = null; 31 | let response = null; 32 | let attempts = 0; 33 | 34 | while (!response && attempts < maxRetries) { 35 | try { 36 | if (!isSilentRequest) { 37 | logger(chalk.blue(`Sending ${method}...`)); 38 | } 39 | 40 | request = await fetch(url, options); 41 | response = await request.json(); 42 | 43 | // Create a unique key in the response object that we can use to get `x-eresult` from. 44 | response.response.___headers = request.headers; // eslint-disable-line no-underscore-dangle 45 | } catch (e) { 46 | logger(`${chalk.bgRed(`${e.name}:`)} ${chalk.red(`For ${method}`)}`, e); 47 | 48 | attempts += 1; 49 | 50 | if (attempts < maxRetries) { 51 | logger(`Retrying ${method} in ${retryDelayMs / 1000} seconds...`); 52 | } else { 53 | throw new SalienScriptException(`Failed to send ${method} after ${attempts} attempts`); 54 | } 55 | 56 | await delay(retryDelayMs); 57 | } 58 | } 59 | 60 | return response.response; 61 | }; 62 | 63 | const getPlayerInfo = async (token, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 64 | const method = 'ITerritoryControlMinigameService/GetPlayerInfo/v0001'; 65 | const params = [`access_token=${token}`]; 66 | const options = { method: 'POST' }; 67 | 68 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 69 | 70 | return response; 71 | }; 72 | 73 | const getPlanets = async (logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 74 | const method = 'ITerritoryControlMinigameService/GetPlanets/v0001'; 75 | const params = ['active_only=1']; 76 | const options = {}; 77 | 78 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 79 | 80 | return response.planets; 81 | }; 82 | 83 | const getPlanet = async (planetId, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 84 | const method = 'ITerritoryControlMinigameService/GetPlanet/v0001'; 85 | const params = [`id=${planetId}`, `language=english`]; 86 | const options = {}; 87 | 88 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 89 | 90 | return response.planets[0]; 91 | }; 92 | 93 | const representClan = async ( 94 | token, 95 | clanId, 96 | logger, 97 | silent, 98 | maxRetries = MAX_RETRIES, 99 | retryDelayMs = RETRY_DELAY_MS, 100 | ) => { 101 | const method = 'ITerritoryControlMinigameService/RepresentClan/v0001'; 102 | const params = [`access_token=${token}`, `clanid=${clanId}`]; 103 | const options = { method: 'POST' }; 104 | 105 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 106 | 107 | return response; 108 | }; 109 | 110 | const leaveGame = async (token, gameId, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 111 | const method = 'IMiniGameService/LeaveGame/v0001'; 112 | const params = [`access_token=${token}`, `gameid=${gameId}`]; 113 | const options = { method: 'POST' }; 114 | 115 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 116 | 117 | return response; 118 | }; 119 | 120 | const joinPlanet = async (token, planetId, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 121 | const method = 'ITerritoryControlMinigameService/JoinPlanet/v0001'; 122 | const params = [`access_token=${token}`, `id=${planetId}`]; 123 | const options = { method: 'POST' }; 124 | 125 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 126 | 127 | return response; 128 | }; 129 | 130 | const joinZone = async (token, zoneId, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 131 | const method = 'ITerritoryControlMinigameService/JoinZone/v0001'; 132 | const params = [`access_token=${token}`, `zone_position=${zoneId}`]; 133 | const options = { method: 'POST' }; 134 | 135 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 136 | 137 | return response; 138 | }; 139 | 140 | const joinBossZone = async (token, zoneId, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 141 | const method = 'ITerritoryControlMinigameService/JoinBossZone/v0001'; 142 | const params = [`access_token=${token}`, `zone_position=${zoneId}`]; 143 | const options = { method: 'POST' }; 144 | 145 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 146 | 147 | return response; 148 | }; 149 | 150 | const reportBossDamage = async ( 151 | token, 152 | useHeal, 153 | damageToBoss, 154 | damageTaken, 155 | logger, 156 | silent, 157 | maxRetries = MAX_RETRIES, 158 | retryDelayMs = RETRY_DELAY_MS, 159 | ) => { 160 | const method = 'ITerritoryControlMinigameService/ReportBossDamage/v0001'; 161 | const params = [ 162 | `access_token=${token}`, 163 | `use_heal_ability=${useHeal}`, 164 | `damage_to_boss=${damageToBoss}`, 165 | `damage_taken=${damageTaken}`, 166 | ]; 167 | const options = { method: 'POST' }; 168 | 169 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 170 | 171 | return response; 172 | }; 173 | 174 | const reportScore = async (token, score, logger, silent, maxRetries = MAX_RETRIES, retryDelayMs = RETRY_DELAY_MS) => { 175 | const method = 'ITerritoryControlMinigameService/ReportScore/v0001'; 176 | const params = [`access_token=${token}`, `score=${score}`, `language=english`]; 177 | const options = { method: 'POST' }; 178 | 179 | const response = await doFetch(method, params, options, logger, silent, maxRetries, retryDelayMs); 180 | 181 | return response; 182 | }; 183 | 184 | module.exports = { 185 | doFetch, 186 | getPlayerInfo, 187 | getPlanets, 188 | getPlanet, 189 | representClan, 190 | leaveGame, 191 | joinPlanet, 192 | joinZone, 193 | joinBossZone, 194 | reportBossDamage, 195 | reportScore, 196 | }; 197 | -------------------------------------------------------------------------------- /src/api/index.test.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const api = require('./index'); 4 | 5 | jest.mock('node-fetch'); 6 | 7 | const token = 'token'; 8 | 9 | describe('api', () => { 10 | describe('getPlayerInfo()', () => { 11 | it('returns a response', async () => { 12 | const mockLogger = jest.fn(); 13 | fetch.mockResolvedValue({ json: async () => ({ response: { a: 1 } }) }); 14 | 15 | const response = await api.getPlayerInfo(token, mockLogger, true, 1, 0); 16 | 17 | expect(response.a).toBe(1); 18 | }); 19 | 20 | it('throws an exception when theres no json()', async () => { 21 | const mockLogger = jest.fn(); 22 | fetch.mockResolvedValue({}); 23 | 24 | try { 25 | await api.getPlayerInfo(token, mockLogger, true, 1, 0); 26 | } catch (e) { 27 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/GetPlayerInfo/v0001 after 1 attempts'); 28 | } 29 | }); 30 | }); 31 | 32 | describe('getPlanets()', () => { 33 | it('returns a response', async () => { 34 | const mockLogger = jest.fn(); 35 | fetch.mockResolvedValue({ json: async () => ({ response: { planets: [1, 2] } }) }); 36 | 37 | const response = await api.getPlanets(mockLogger, true, 1, 0); 38 | 39 | expect(response.length).toBe(2); 40 | }); 41 | 42 | it('throws an exception when theres no json()', async () => { 43 | const mockLogger = jest.fn(); 44 | fetch.mockResolvedValue({}); 45 | 46 | try { 47 | await api.getPlanets(mockLogger, true, 1, 0); 48 | } catch (e) { 49 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/GetPlanets/v0001 after 1 attempts'); 50 | } 51 | }); 52 | }); 53 | 54 | describe('getPlanet()', () => { 55 | const planetId = 1234; 56 | 57 | it('returns a response', async () => { 58 | const mockLogger = jest.fn(); 59 | fetch.mockResolvedValue({ json: async () => ({ response: { planets: [1] } }) }); 60 | 61 | const response = await api.getPlanet(planetId, mockLogger, true, 1, 0); 62 | 63 | expect(response).toBe(1); 64 | }); 65 | 66 | it('throws an exception when theres no json()', async () => { 67 | const mockLogger = jest.fn(); 68 | fetch.mockResolvedValue({}); 69 | 70 | try { 71 | await api.getPlanet(planetId, mockLogger, true, 1, 0); 72 | } catch (e) { 73 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/GetPlanet/v0001 after 1 attempts'); 74 | } 75 | }); 76 | }); 77 | 78 | describe('representClan()', () => { 79 | const clanId = 1234; 80 | 81 | it('returns a response', async () => { 82 | const mockLogger = jest.fn(); 83 | fetch.mockResolvedValue({ json: async () => ({ response: { a: 1 } }) }); 84 | 85 | const response = await api.representClan(token, clanId, mockLogger, true, 1, 0); 86 | 87 | expect(response.a).toBe(1); 88 | }); 89 | 90 | it('throws an exception when theres no json()', async () => { 91 | const mockLogger = jest.fn(); 92 | fetch.mockResolvedValue({}); 93 | 94 | try { 95 | await api.representClan(token, clanId, mockLogger, true, 1, 0); 96 | } catch (e) { 97 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/RepresentClan/v0001 after 1 attempts'); 98 | } 99 | }); 100 | }); 101 | 102 | describe('leaveGame()', () => { 103 | const gameId = 1234; 104 | 105 | it('returns a response', async () => { 106 | const mockLogger = jest.fn(); 107 | fetch.mockResolvedValue({ json: async () => ({ response: { a: 1 } }) }); 108 | 109 | const response = await api.leaveGame(token, gameId, mockLogger, true, 1, 0); 110 | 111 | expect(response.a).toBe(1); 112 | }); 113 | 114 | it('throws an exception when theres no json()', async () => { 115 | const mockLogger = jest.fn(); 116 | fetch.mockResolvedValue({}); 117 | 118 | try { 119 | await api.leaveGame(token, gameId, mockLogger, true, 1, 0); 120 | } catch (e) { 121 | expect(e.message).toBe('Failed to send IMiniGameService/LeaveGame/v0001 after 1 attempts'); 122 | } 123 | }); 124 | }); 125 | 126 | describe('joinPlanet()', () => { 127 | const planetId = 1234; 128 | 129 | it('returns a response', async () => { 130 | const mockLogger = jest.fn(); 131 | fetch.mockResolvedValue({ json: async () => ({ response: { a: 1 } }) }); 132 | 133 | const response = await api.joinPlanet(token, planetId, mockLogger, true, 1, 0); 134 | 135 | expect(response.a).toBe(1); 136 | }); 137 | 138 | it('throws an exception when theres no json()', async () => { 139 | const mockLogger = jest.fn(); 140 | fetch.mockResolvedValue({}); 141 | 142 | try { 143 | await api.joinPlanet(token, planetId, mockLogger, true, 1, 0); 144 | } catch (e) { 145 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/JoinPlanet/v0001 after 1 attempts'); 146 | } 147 | }); 148 | }); 149 | 150 | describe('joinZone()', () => { 151 | const zoneId = 1234; 152 | 153 | it('returns a response', async () => { 154 | const mockLogger = jest.fn(); 155 | fetch.mockResolvedValue({ json: async () => ({ response: { a: 1 } }) }); 156 | 157 | const response = await api.joinZone(token, zoneId, mockLogger, true, 1, 0); 158 | 159 | expect(response.a).toBe(1); 160 | }); 161 | 162 | it('throws an exception when theres no json()', async () => { 163 | const mockLogger = jest.fn(); 164 | fetch.mockResolvedValue({}); 165 | 166 | try { 167 | await api.joinZone(token, zoneId, mockLogger, true, 1, 0); 168 | } catch (e) { 169 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/JoinZone/v0001 after 1 attempts'); 170 | } 171 | }); 172 | }); 173 | 174 | describe('reportScore()', () => { 175 | const score = 1234; 176 | 177 | it('returns a response', async () => { 178 | const mockLogger = jest.fn(); 179 | fetch.mockResolvedValue({ json: async () => ({ response: { a: 1 } }) }); 180 | 181 | const response = await api.reportScore(token, score, mockLogger, true, 1, 0); 182 | 183 | expect(response.a).toBe(1); 184 | }); 185 | 186 | it('throws an exception when theres no json()', async () => { 187 | const mockLogger = jest.fn(); 188 | fetch.mockResolvedValue({}); 189 | 190 | try { 191 | await api.reportScore(token, score, mockLogger, true, 1, 0); 192 | } catch (e) { 193 | expect(e.message).toBe('Failed to send ITerritoryControlMinigameService/ReportScore/v0001 after 1 attempts'); 194 | } 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /src/exceptions.js: -------------------------------------------------------------------------------- 1 | class SalienScriptException { 2 | constructor(message) { 3 | this.name = 'SalienScriptException'; 4 | this.message = message; 5 | } 6 | } 7 | 8 | class SalienScriptRestart { 9 | constructor(message) { 10 | this.name = 'SalienScriptRestart'; 11 | this.message = message; 12 | } 13 | } 14 | 15 | module.exports = { 16 | SalienScriptException, 17 | SalienScriptRestart, 18 | }; 19 | -------------------------------------------------------------------------------- /src/game/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | const { getPlanet } = require('../api/index'); 4 | const { SalienScriptException } = require('../exceptions'); 5 | const { getPercentage } = require('../util'); 6 | 7 | const formatPlanetName = name => 8 | name 9 | .replace('#TerritoryControl_Planet', '') 10 | .split('_') 11 | .join(' '); 12 | 13 | const getZoneDifficultyName = ({ type, difficulty }) => { 14 | const boss = type === 4 ? 'BOSS - ' : ''; 15 | let name = ''; 16 | 17 | switch (difficulty) { 18 | /* eslint-disable prettier/prettier */ 19 | case 3: name = 'Hard'; break; 20 | case 2: name = 'Medium'; break; 21 | case 1: name = 'Easy'; break; 22 | default: name = difficulty; 23 | /* eslint-enable prettier/prettier */ 24 | } 25 | 26 | return `${boss}${name}`; 27 | }; 28 | 29 | const getScoreForZone = ({ difficulty }) => { 30 | let score; 31 | 32 | switch (difficulty) { 33 | /* eslint-disable prettier/prettier */ 34 | case 1: score = 5; break; 35 | case 2: score = 10; break; 36 | case 3: score = 20; break; 37 | // Set fallback score equal to high zone score to avoid uninitialized variable if new 38 | // zone difficulty is introduced (e.g., for boss zones) 39 | default: score = 20; 40 | /* eslint-enable prettier/prettier */ 41 | } 42 | 43 | return score * 120; 44 | }; 45 | 46 | const getAllPlanetStates = async (planets, completionCutoff, logger, isSilentRequest) => { 47 | if (!planets) { 48 | throw new SalienScriptException('No planets provided.'); 49 | } 50 | 51 | if (!completionCutoff) { 52 | throw new SalienScriptException('No completion cut-off percent given.'); 53 | } 54 | 55 | logger(`Scanning all planets for next best zone...`); 56 | 57 | const knownPlanets = new Map(); 58 | 59 | try { 60 | // Patch the apiGetPlanets response with zones from apiGetPlanet 61 | const mappedPlanets = await Promise.all( 62 | planets.map(async planet => { 63 | const object = { ...planet }; 64 | 65 | const currentPlanet = await getPlanet(planet.id, logger, isSilentRequest); 66 | 67 | object.zones = currentPlanet.zones; 68 | 69 | return object; 70 | }), 71 | ); 72 | 73 | mappedPlanets.forEach(planet => { 74 | let numHardZones = 0; 75 | let numMediumZones = 0; 76 | let numEasyZones = 0; 77 | let numUnknownZones = 0; 78 | 79 | let cleanZones = []; 80 | const bossZones = []; 81 | 82 | planet.zones.forEach(zone => { 83 | const { capture_progress: captureProgress, captured, type, difficulty, gameid } = zone; 84 | 85 | // disregard this zone if there is no gameid, it's close to being captured or already captured 86 | if (!gameid || (captureProgress && captureProgress > completionCutoff) || captured || captureProgress === 0) { 87 | return; 88 | } 89 | 90 | // store that it's a boss zone 91 | if (type === 4) { 92 | bossZones.push(zone); 93 | } else if (type !== 3) { 94 | logger(chalk.red(`!! Unknown zone type found: ${type}`)); 95 | } 96 | 97 | // count the difficulties of this planet's zones 98 | switch (difficulty) { 99 | /* eslint-disable prettier/prettier */ 100 | case 3: numHardZones += 1; break; 101 | case 2: numMediumZones += 1; break; 102 | case 1: numEasyZones += 1; break; 103 | default: numUnknownZones += 1; 104 | /* eslint-enable prettier/prettier */ 105 | } 106 | 107 | cleanZones.push(zone); 108 | }); 109 | 110 | // if we have boss zones, use them over anything else 111 | if (bossZones.length > 0) { 112 | cleanZones = bossZones; 113 | } 114 | 115 | // sort the clean zones by most difficult and then by capture progress 116 | cleanZones.sort((a, b) => { 117 | if (b.difficulty === a.difficulty) { 118 | if (a.capture_progress * 100 !== b.capture_progress * 100) { 119 | return a.capture_progress * 100000 - b.capture_progress * 100000; 120 | } 121 | 122 | return b.zone_position - a.zone_position; 123 | } 124 | 125 | return b.difficulty - a.difficulty; 126 | }); 127 | 128 | knownPlanets.set(planet.id, { 129 | numHardZones, 130 | numMediumZones, 131 | numEasyZones, 132 | numUnknownZones, 133 | bestZone: cleanZones[0], 134 | bossZones, 135 | ...planet, 136 | }); 137 | 138 | const planetName = formatPlanetName(planet.state.name); 139 | const planetCapturePercent = getPercentage(planet.state.capture_progress); 140 | const planetCurrentPlayers = planet.state.current_players.toLocaleString(); 141 | 142 | let planetInfo = `>> Planet ${chalk.green(`${planet.id}`.padStart(3))}`; 143 | planetInfo += ` (Captured: ${chalk.yellow(`${planetCapturePercent}%`.padStart(6))})`; 144 | planetInfo += ` - Hard: ${chalk.yellow(`${numHardZones}`.padStart(2))}`; 145 | planetInfo += ` - Medium: ${chalk.yellow(`${numMediumZones}`.padStart(2))}`; 146 | planetInfo += ` - Easy: ${chalk.yellow(`${numEasyZones}`.padStart(2))}`; 147 | planetInfo += ` - Players: ${chalk.yellow(`${planetCurrentPlayers}`.padStart(7))}`; 148 | planetInfo += ` (${chalk.green(planetName)})`; 149 | planetInfo += numUnknownZones 150 | ? `\n${chalk.yellow(`!! ${`${numUnknownZones}`.padStart(2)} unknown zones found in planet`)}` 151 | : ''; 152 | planetInfo += bossZones.length > 0 ? `\n${chalk.yellow(`!! Boss zone detected`)}` : ''; 153 | 154 | logger(planetInfo); 155 | }); 156 | } catch (e) { 157 | throw e; 158 | } 159 | 160 | return knownPlanets; 161 | }; 162 | 163 | const getBestPlanetAndZone = async (planets, logger) => { 164 | const planetsWithSortKeys = []; 165 | let foundBoss = false; 166 | let selectedPlanet = null; 167 | 168 | planets.forEach(planet => { 169 | if (foundBoss) { 170 | return; 171 | } 172 | 173 | if (planet.bestZone.type === 4) { 174 | logger(chalk.green(`>> Planet ${planet.id} has an uncaptured boss zone, selecting it`)); 175 | foundBoss = true; 176 | selectedPlanet = planet; 177 | return; 178 | } 179 | 180 | let sortKey = 0; 181 | 182 | if (planet.numEasyZones > 0) { 183 | sortKey = 99 - planet.numEasyZones; 184 | } 185 | 186 | if (planet.numMediumZones > 0) { 187 | sortKey = 10 ** 2 * (99 - planet.numMediumZones); 188 | } 189 | 190 | if (planet.numHardZones > 0) { 191 | sortKey = 10 ** 4 * (99 - planet.numHardZones); 192 | } 193 | 194 | planetsWithSortKeys.push({ ...planet, sortKey }); 195 | }); 196 | 197 | // early return 198 | if (selectedPlanet) { 199 | return selectedPlanet; 200 | } 201 | 202 | return planetsWithSortKeys.sort((a, b) => b.sortKey - a.sortKey)[0]; 203 | }; 204 | 205 | module.exports = { getZoneDifficultyName, getScoreForZone, getAllPlanetStates, getBestPlanetAndZone }; 206 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2018 Alex Gabites 5 | * 6 | * https://github.com/South-Paw/salien-script-js 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in all 16 | * copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | * SOFTWARE. 25 | */ 26 | 27 | const chalk = require('chalk'); 28 | const delay = require('delay'); 29 | 30 | const { 31 | getPlayerInfo, 32 | getPlanets, 33 | getPlanet, 34 | representClan, 35 | leaveGame, 36 | joinPlanet, 37 | joinZone, 38 | joinBossZone, 39 | reportBossDamage, 40 | reportScore, 41 | } = require('./api/index'); 42 | const { getZoneDifficultyName, getScoreForZone, getAllPlanetStates, getBestPlanetAndZone } = require('./game/index'); 43 | const { SalienScriptRestart } = require('./exceptions'); 44 | const { getPercentage, updateCheck, utilLogger } = require('./util'); 45 | const pkg = require('../package.json'); 46 | 47 | class SalienScript { 48 | constructor({ token, clan, name = null, logRequests = false }) { 49 | // user defined variables 50 | this.token = token; 51 | this.clanId = clan; 52 | this.name = name; 53 | this.isSilentRequest = !logRequests; 54 | 55 | // script variables 56 | this.startTime = null; 57 | this.knownPlanets = new Map(); 58 | this.currentPlanetAndZone = null; 59 | this.steamThinksPlanet = null; 60 | this.lastKnownPlanetId = null; 61 | 62 | // script variables that don't get reset 63 | this.clanCheckDone = false; 64 | 65 | // script defaults 66 | this.gameWaitTimeSec = 110; 67 | this.defaultDelayMs = 5000; 68 | this.defaultDelaySec = this.defaultDelayMs / 1000; 69 | this.cutoff = 0.99; 70 | this.defaultAllowedBossFails = 10; 71 | } 72 | 73 | resetScript() { 74 | this.startTime = null; 75 | this.knownPlanets = new Map(); 76 | this.currentPlanetAndZone = null; 77 | this.steamThinksPlanet = null; 78 | this.lastKnownPlanetId = null; 79 | } 80 | 81 | logger(message, error) { 82 | utilLogger(this.name, { message, error }); 83 | } 84 | 85 | async apiGetPlayerInfo() { 86 | return getPlayerInfo(this.token, (m, e) => this.logger(m, e), this.isSilentRequest); 87 | } 88 | 89 | async apiGetPlanets() { 90 | return getPlanets((m, e) => this.logger(m, e), this.isSilentRequest); 91 | } 92 | 93 | async apiGetPlanet(planetId) { 94 | return getPlanet(planetId, (m, e) => this.logger(m, e), this.isSilentRequest); 95 | } 96 | 97 | async apiRepresentClan(clanId) { 98 | return representClan(this.token, clanId, (m, e) => this.logger(m, e), this.isSilentRequest); 99 | } 100 | 101 | async apiLeaveGame(gameId) { 102 | return leaveGame(this.token, gameId, (m, e) => this.logger(m, e), this.isSilentRequest); 103 | } 104 | 105 | async apiJoinPlanet(planetId) { 106 | return joinPlanet(this.token, planetId, (m, e) => this.logger(m, e), this.isSilentRequest); 107 | } 108 | 109 | async apiJoinZone(zoneId) { 110 | return joinZone(this.token, zoneId, (m, e) => this.logger(m, e), this.isSilentRequest); 111 | } 112 | 113 | async apiJoinBossZone(zoneId) { 114 | return joinBossZone(this.token, zoneId, (m, e) => this.logger(m, e), this.isSilentRequest); 115 | } 116 | 117 | async apiReportBossDamage(useHeal, damageToBoss, damageTaken) { 118 | return reportBossDamage( 119 | this.token, 120 | useHeal, 121 | damageToBoss, 122 | damageTaken, 123 | (m, e) => this.logger(m, e), 124 | this.isSilentRequest, 125 | ); 126 | } 127 | 128 | async apiReportScore(score) { 129 | return reportScore(this.token, score, (m, e) => this.logger(m, e), this.isSilentRequest); 130 | } 131 | 132 | async leaveCurrentGame(requestedPlanetId = 0) { 133 | const playerInfo = await this.apiGetPlayerInfo(); 134 | 135 | if (playerInfo.active_boss_game) { 136 | await this.apiLeaveGame(playerInfo.active_boss_game); 137 | } 138 | 139 | if (playerInfo.active_zone_game) { 140 | await this.apiLeaveGame(playerInfo.active_zone_game); 141 | } 142 | 143 | if (!playerInfo.active_planet) { 144 | return 0; 145 | } 146 | 147 | const newPlayerInfo = await this.apiGetPlayerInfo(); 148 | const activePlanet = newPlayerInfo.active_planet; 149 | 150 | if (requestedPlanetId > 0 && requestedPlanetId !== activePlanet) { 151 | let message = `>> Leaving planet ${chalk.yellow(activePlanet)}, because`; 152 | message += ` we want to be on ${chalk.yellow(requestedPlanetId)}`; 153 | 154 | this.logger(message); 155 | 156 | await this.apiLeaveGame(activePlanet); 157 | } 158 | 159 | return activePlanet; 160 | } 161 | 162 | async doClanSetup() { 163 | let playerInfo = await this.apiGetPlayerInfo(); 164 | 165 | if (this.clanId && !this.clanCheckDone && playerInfo.clan_info) { 166 | this.logger(`Attempting to join group id: ${chalk.yellow(this.clanId)}`); 167 | 168 | await this.apiRepresentClan(this.clanId); 169 | 170 | playerInfo = await this.apiGetPlayerInfo(); 171 | 172 | if (playerInfo.clan_info) { 173 | this.logger(chalk.bgCyan(` Joined group: ${playerInfo.clan_info.name} `)); 174 | this.logger(chalk.yellow("If the name above isn't expected, check if you're actually a member of that group")); 175 | 176 | this.clanCheckDone = true; 177 | } 178 | 179 | console.log(''); // eslint-disable-line no-console 180 | } 181 | } 182 | 183 | async doGameSetup() { 184 | const planets = await this.apiGetPlanets(); 185 | 186 | this.knownPlanets = await getAllPlanetStates( 187 | planets, 188 | this.cutoff, 189 | (m, e) => this.logger(m, e), 190 | this.isSilentRequest, 191 | ); 192 | 193 | this.currentPlanetAndZone = await getBestPlanetAndZone(this.knownPlanets, (m, e) => this.logger(m, e)); 194 | 195 | const zoneCapturePercent = getPercentage(this.currentPlanetAndZone.bestZone.capture_progress); 196 | 197 | let zoneMsg = `>> Selected Next Zone ${chalk.green(this.currentPlanetAndZone.bestZone.zone_position)}`; 198 | zoneMsg += ` on Planet ${chalk.green(this.currentPlanetAndZone.id)}`; 199 | zoneMsg += ` (Captured: ${chalk.yellow(`${zoneCapturePercent}%`.padStart(6))}`; 200 | zoneMsg += ` - Difficulty: ${chalk.yellow(getZoneDifficultyName(this.currentPlanetAndZone.bestZone))})`; 201 | 202 | this.logger(zoneMsg); 203 | 204 | console.log(''); // eslint-disable-line no-console 205 | } 206 | 207 | async playBossZone() { 208 | const healMin = 0; 209 | const healMax = 120; 210 | 211 | // Avoid first time not sync error 212 | await delay(4000); 213 | 214 | let allowedBossFails = this.defaultAllowedBossFails; 215 | let nextHeal = Number.MAX_SAFE_INTEGER; 216 | let waitingForPlayers = true; 217 | 218 | const oldPlayerInfo = await this.apiGetPlayerInfo(); 219 | 220 | // eslint-disable-next-line no-constant-condition 221 | while (true) { 222 | let useHeal = 0; 223 | const damageToBoss = waitingForPlayers ? 0 : Math.floor(Math.random() * (150 - 30 + 1) + 30); 224 | const damageTaken = 0; 225 | 226 | if (Math.floor(new Date().getTime() / 1000) >= nextHeal) { 227 | useHeal = 1; 228 | nextHeal = Math.floor(new Date().getTime() / 1000) + 120; 229 | 230 | this.logger(chalk.green('@@ Boss -- Using heal ability')); 231 | } 232 | 233 | const report = await this.apiReportBossDamage(useHeal, damageToBoss, damageTaken); 234 | 235 | // eslint-disable-next-line no-underscore-dangle 236 | if (Number(report.___headers.get('x-eresult')) === 11) { 237 | throw new SalienScriptRestart('Recieved invalid boss state!'); 238 | } 239 | 240 | // eslint-disable-next-line no-underscore-dangle 241 | if (Number(report.___headers.get('x-eresult')) !== 1 && Number(report.___headers.get('x-eresult')) !== 93) { 242 | allowedBossFails -= 1; 243 | 244 | if (allowedBossFails < 1) { 245 | this.logger(chalk.green('@@ Boss -- Battle had too many errors!')); 246 | 247 | break; 248 | } 249 | } 250 | 251 | // if we didn't get an error, reset the allowed failure count 252 | allowedBossFails = this.defaultAllowedBossFails; 253 | 254 | if (report.waiting_for_players) { 255 | this.logger(chalk.green('@@ Boss -- Waiting for players...')); 256 | 257 | await delay(this.defaultDelayMs); 258 | 259 | continue; // eslint-disable-line no-continue 260 | } else if (waitingForPlayers) { 261 | waitingForPlayers = false; 262 | nextHeal = 263 | Math.floor(new Date().getTime() / 1000) + Math.floor(Math.random() * (healMax - healMin + 1) + healMin); 264 | } 265 | 266 | if (!report.boss_status) { 267 | this.logger('@@ Boss -- Waiting...'); 268 | 269 | await delay(this.defaultDelayMs); 270 | 271 | continue; // eslint-disable-line no-continue 272 | } 273 | 274 | if (report.boss_status.boss_players) { 275 | console.log(''); // eslint-disable-line no-console 276 | 277 | report.boss_status.boss_players.forEach(player => { 278 | // eslint-disable-next-line no-control-regex 279 | let scoreCard = ` ${`${player.name.replace(/[^\x00-\x7F]/g, '')}`.padEnd(30)}`; 280 | scoreCard += ` - HP: ${chalk.yellow(`${player.hp}`.padStart(6))} / ${`${player.max_hp}`.padStart(6)}`; 281 | scoreCard += ` - XP Gained: ${chalk.yellow(`${Number(player.xp_earned).toLocaleString()}`.padStart(12))}`; 282 | this.logger(scoreCard); 283 | }); 284 | 285 | console.log(''); // eslint-disable-line no-console 286 | } 287 | 288 | if (report.game_over) { 289 | this.logger(chalk.green('@@ Boss -- The battle has ended!')); 290 | 291 | break; 292 | } 293 | 294 | let bossStatusMsg = `@@ Boss -- HP: ${Number(report.boss_status.boss_hp).toLocaleString()}`; 295 | bossStatusMsg += ` / ${Number(report.boss_status.boss_max_hp).toLocaleString()}`; 296 | bossStatusMsg += ` (${getPercentage(report.boss_status.boss_hp / report.boss_status.boss_max_hp)}%)`; 297 | bossStatusMsg += ` - Lasers: ${report.num_laser_uses}`; 298 | bossStatusMsg += ` - Team Heals: ${report.num_team_heals}`; 299 | 300 | this.logger(bossStatusMsg); 301 | 302 | await delay(this.defaultDelayMs); 303 | 304 | console.log(''); // eslint-disable-line no-console 305 | } 306 | 307 | const newPlayerInfo = await this.apiGetPlayerInfo(); 308 | 309 | if (newPlayerInfo.score) { 310 | const newXp = Number(newPlayerInfo.score) - Number(oldPlayerInfo.score); 311 | 312 | let bossReport = `++ Your score after boss battle:`; 313 | bossReport += ` ${chalk.yellow(Number(newPlayerInfo.score).toLocaleString())}`; 314 | bossReport += ` (+ ${chalk.yellow(`${newXp.toLocaleString()}`)} XP)`; 315 | bossReport += ` - Current Level: ${chalk.green(newPlayerInfo.level)}`; 316 | 317 | this.logger(bossReport); 318 | } 319 | 320 | if (newPlayerInfo.active_boss_game) { 321 | await this.apiLeaveGame(newPlayerInfo.active_boss_game); 322 | } 323 | 324 | if (newPlayerInfo.active_planet) { 325 | await this.apiLeaveGame(newPlayerInfo.active_planet); 326 | } 327 | } 328 | 329 | async playNormalZone(zone) { 330 | const { zone_info: zoneInfo } = zone; 331 | 332 | const zoneCapturePercent = getPercentage(zoneInfo.capture_progress); 333 | 334 | let joinMsg = `>> Joined Zone ${chalk.green(zoneInfo.zone_position)}`; 335 | joinMsg += ` on Planet ${chalk.green(this.currentPlanetAndZone.id)}`; 336 | joinMsg += ` (Captured: ${chalk.yellow(`${zoneCapturePercent}%`.padStart(6))}`; 337 | joinMsg += ` - Difficulty: ${chalk.yellow(getZoneDifficultyName(this.currentPlanetAndZone.bestZone))})`; 338 | 339 | this.logger(joinMsg); 340 | 341 | if (zoneInfo.top_clans) { 342 | this.logger(`-- Top Clans:${zoneInfo.top_clans.map(({ name }) => ` ${name}`)}`); 343 | } 344 | 345 | console.log(''); // eslint-disable-line no-console 346 | this.logger(`${chalk.bgMagenta(` Waiting ${this.gameWaitTimeSec} seconds for round to finish... `)}`); 347 | 348 | // 10 seconds before the score is reported, get the next planet and zone we should focus on. 349 | setTimeout(async () => { 350 | await this.doGameSetup(); 351 | }, (this.gameWaitTimeSec - 10) * 1000); 352 | 353 | await delay(this.gameWaitTimeSec * 1000); 354 | 355 | const score = await this.apiReportScore(getScoreForZone(zoneInfo)); 356 | 357 | // cause the game's api returns some numbers as strings and others as numbers 358 | const oldScore = Number(score.old_score); 359 | const newScore = Number(score.new_score); 360 | const nextLevelScore = Number(score.next_level_score); 361 | const newLevel = Number(score.new_level); 362 | 363 | if (newScore) { 364 | const earnedXp = newScore - oldScore; 365 | const nextLevelPercent = getPercentage(newScore / nextLevelScore); 366 | 367 | console.log(''); // eslint-disable-line no-console 368 | 369 | let currentLevelMsg = `>> Score: ${chalk.cyan(Number(newScore).toLocaleString())}`; 370 | currentLevelMsg += ` (${chalk.green(`+${earnedXp.toLocaleString()}`)})`; 371 | currentLevelMsg += ` - Current Level: ${chalk.green(newLevel)} (${nextLevelPercent}%)`; 372 | 373 | this.logger(currentLevelMsg); 374 | 375 | const remainingXp = nextLevelScore - newScore; 376 | 377 | const timeRemaining = ((nextLevelScore - newScore) / getScoreForZone(zoneInfo)) * (this.gameWaitTimeSec / 60); 378 | const hoursRemaining = Math.floor(timeRemaining / 60); 379 | const minutesRemaining = Math.round(timeRemaining % 60); 380 | const levelEta = `${hoursRemaining}h ${hoursRemaining === 0 && minutesRemaining === 0 ? 2 : minutesRemaining}m`; 381 | 382 | let nextLevelMsg = `>> Next Level: ${chalk.yellow(nextLevelScore.toLocaleString())} XP`; 383 | nextLevelMsg += ` - Remaining: ${chalk.yellow(remainingXp.toLocaleString())} XP`; 384 | nextLevelMsg += ` - ETA: ${chalk.green(levelEta)}\n`; 385 | 386 | this.logger(nextLevelMsg); 387 | } 388 | 389 | const leavingGame = await this.leaveCurrentGame(this.currentPlanetAndZone.id); 390 | 391 | if (leavingGame !== this.currentPlanetAndZone.id) { 392 | throw new SalienScriptRestart(`!! Wrong current Planet ${chalk.yellow(leavingGame)}`); 393 | } 394 | } 395 | 396 | async doGameLoop() { 397 | if (!this.currentPlanetAndZone) { 398 | await this.doGameSetup(); 399 | } 400 | 401 | if (this.lastKnownPlanetId !== this.currentPlanetAndZone.id) { 402 | while (this.currentPlanetAndZone.id !== this.steamThinksPlanet) { 403 | this.steamThinksPlanet = await this.leaveCurrentGame(this.currentPlanetAndZone.id); 404 | 405 | if (this.currentPlanetAndZone.id !== this.steamThinksPlanet) { 406 | await this.apiJoinPlanet(this.currentPlanetAndZone.id); 407 | 408 | this.steamThinksPlanet = await this.leaveCurrentGame(); 409 | } 410 | } 411 | 412 | this.lastKnownPlanetId = this.currentPlanetAndZone.id; 413 | } 414 | 415 | let zone; 416 | 417 | // if the best zone is a boss zone 418 | if (this.currentPlanetAndZone.bestZone.boss_active) { 419 | zone = await this.apiJoinBossZone(this.currentPlanetAndZone.bestZone.zone_position); 420 | 421 | // eslint-disable-next-line no-underscore-dangle 422 | if (Number(zone.___headers.get('x-eresult')) !== 1) { 423 | throw new SalienScriptRestart('!! Failed to join boss zone', zone); 424 | } 425 | 426 | await this.playBossZone(); 427 | 428 | return; 429 | } 430 | 431 | // otherwise we're in a normal zone and play the game normally 432 | zone = await this.apiJoinZone(this.currentPlanetAndZone.bestZone.zone_position); 433 | 434 | if (!zone.zone_info) { 435 | throw new SalienScriptRestart('!! Failed to join a zone', zone); 436 | } 437 | 438 | await this.playNormalZone(zone); 439 | } 440 | 441 | async init() { 442 | this.resetScript(); 443 | 444 | this.startTime = new Date().getTime(); 445 | 446 | console.log(''); // eslint-disable-line no-console 447 | this.logger(chalk.bgGreen(` Started SalienScript | Version: ${pkg.version} `)); 448 | this.logger(chalk.bgCyan(' Thanks for choosing https://github.com/South-Paw/salien-script-js ')); 449 | this.logger(chalk.bgCyan(' Remeber to drop us a ⭐ star on the project if you appreciate this script! ')); 450 | console.log(''); // eslint-disable-line no-console 451 | 452 | try { 453 | await this.doClanSetup(); 454 | 455 | await this.doGameSetup(); 456 | 457 | // eslint-disable-next-line no-constant-condition 458 | while (true) { 459 | await updateCheck(this.name); 460 | 461 | await this.doGameLoop(); 462 | } 463 | } catch (e) { 464 | this.logger(`${chalk.bgRed(`${e.name}:`)} ${chalk.red(e.message)}`, e.name !== 'SalienScriptRestart' ? e : null); 465 | this.logger(`${chalk.bgMagenta(` Script will restart in ${this.defaultDelaySec} seconds... `)}\n\n`); 466 | 467 | await delay(this.defaultDelayMs); 468 | 469 | this.init(); 470 | } 471 | } 472 | } 473 | 474 | module.exports = SalienScript; 475 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const dateFormat = require('dateformat'); 3 | const checkForUpdate = require('update-check'); 4 | 5 | const pkg = require('../package.json'); 6 | 7 | const debug = message => console.log(`${JSON.stringify(message, 0, 2)}`); // eslint-disable-line no-console 8 | 9 | const utilLogger = (name, msg) => { 10 | const { message, error } = msg; 11 | 12 | let prefix = chalk.white(dateFormat(new Date(), '[HH:MM:ss]')); 13 | 14 | if (name) { 15 | prefix += ` (${name})`; 16 | } 17 | 18 | // TODO: maybe do some fancy indentation logic here...? check first char for '>' or '-' and indent if not found? 19 | // be aware that chalk is adding colors tho and they show up if you debug the `message` object 20 | 21 | console.log(prefix, message); // eslint-disable-line no-console 22 | 23 | if (error) { 24 | debug(error); 25 | } 26 | }; 27 | 28 | const getPercentage = number => 29 | Number(number * 100) 30 | .toFixed(2) 31 | .toString(); 32 | 33 | const updateCheck = async name => { 34 | let hasUpdate = null; 35 | 36 | try { 37 | hasUpdate = await checkForUpdate(pkg, { interval: 120000 }); 38 | } catch (err) { 39 | const updateMsg = `${chalk.bgRed(' UpdateCheck ')} ${chalk.red(`Error while checking for updates: ${err}`)}`; 40 | 41 | utilLogger(name, { message: updateMsg, error: err }); 42 | } 43 | 44 | if (await hasUpdate) { 45 | let hasUpdateMsg = `${chalk.bgMagenta(' UpdateCheck ')} `; 46 | hasUpdateMsg += `The latest version is ${chalk.bgCyan(hasUpdate.latest)}. Please update!`; 47 | 48 | utilLogger(name, { message: hasUpdateMsg }); 49 | 50 | let howToUpdate = `${chalk.bgMagenta(' UpdateCheck ')} `; 51 | howToUpdate += `To update, stop this script and run: ${chalk.bgCyan('npm i -g salien-script-js')}`; 52 | 53 | utilLogger(name, { message: howToUpdate }); 54 | 55 | console.log(''); // eslint-disable-line no-console 56 | } 57 | }; 58 | 59 | module.exports = { 60 | debug, 61 | utilLogger, 62 | getPercentage, 63 | updateCheck, 64 | }; 65 | --------------------------------------------------------------------------------