├── .gitignore ├── docs ├── images │ ├── follow_example.gif │ └── pickit_example.gif ├── HISTORY.md └── API.md ├── lib ├── plugins │ ├── chat.js │ ├── exit.js │ ├── websocketApi.js │ ├── players.js │ ├── skills.js │ ├── stats.js │ ├── farm.js │ ├── attack.js │ ├── commands.js │ ├── town.js │ ├── dumps.md │ ├── inventory.js │ └── position.js ├── utils.js ├── map.js └── pathFinding.js ├── .circleci └── config.yml ├── package.json ├── LICENSE ├── README.md ├── examples └── bot.js ├── pickitExample.json ├── index.js └── test ├── testPickit.js └── testPathFinding.js /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | .idea 4 | -------------------------------------------------------------------------------- /docs/images/follow_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MephisTools/AutoTathamet/HEAD/docs/images/follow_example.gif -------------------------------------------------------------------------------- /docs/images/pickit_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MephisTools/AutoTathamet/HEAD/docs/images/pickit_example.gif -------------------------------------------------------------------------------- /docs/HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 2 | 3 | * standard is a dev dep 4 | 5 | ## 1.0.0 6 | 7 | * basic functionality is there : movement, chat, some inventory support -------------------------------------------------------------------------------- /lib/plugins/chat.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | bot.say = (message) => { // TODO: Maybe an option to whisper the master 3 | bot._client.write('D2GS_CHATMESSAGE', { 4 | type: 1, 5 | unk1: 0, 6 | unk2: 0, 7 | message: message 8 | }) 9 | } 10 | } 11 | 12 | module.exports = inject 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | build: 5 | parallelism: 1 6 | docker: 7 | - image: circleci/node:10 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | key: dependency-cache-{{ checksum "package.json" }} 12 | - run: npm i 13 | - save_cache: 14 | key: dependency-cache-{{ checksum "package.json" }} 15 | paths: 16 | - ./node_modules 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /lib/plugins/exit.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | // Just leave the game, not tested 3 | bot.exit = () => { // Clear all variables? 4 | bot._client.write('D2GS_GAMEEXIT', {}) 5 | bot._client.write('SID_LEAVEGAME', {}) 6 | bot.playerList = [] 7 | } 8 | 9 | process.on('SIGINT', () => { 10 | bot._client.write('D2GS_GAMEEXIT', {}) 11 | bot._client.write('SID_LEAVEGAME', {}) 12 | 13 | setTimeout(() => process.exit(), 2000) 14 | }) 15 | } 16 | 17 | module.exports = inject 18 | -------------------------------------------------------------------------------- /lib/plugins/websocketApi.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | const WebSocket = require('ws') 3 | 4 | bot.wss = new WebSocket.Server({ port: 8080 }) 5 | 6 | bot.wss.broadcast = function broadcast (data) { 7 | bot.wss.clients.forEach(function each (client) { 8 | if (client.readyState === WebSocket.OPEN) { 9 | client.send(data) 10 | } 11 | }) 12 | } 13 | 14 | bot._client.on('packet', ({ protocol, name, params }) => { 15 | bot.wss.broadcast(JSON.stringify({ protocol, name, params })) 16 | }) 17 | bot._client.on('sentPacket', ({ protocol, name, params }) => { 18 | bot.wss.broadcast(JSON.stringify({ protocol, name, params })) 19 | }) 20 | 21 | bot.wss.on('connection', (ws) => { 22 | ws.on('message', (message) => { 23 | console.log(`received from web client ${message}`) 24 | bot._client.write(message) 25 | }) 26 | }) 27 | } 28 | 29 | module.exports = inject 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autotathamet", 3 | "version": "1.0.1", 4 | "description": "Create Diablo2 bots with a powerful, stable, and high level JavaScript API.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint", 8 | "lint": "standard", 9 | "fix": "standard --fix" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/MephisTools/AutoTathamet.git" 14 | }, 15 | "keywords": [ 16 | "bot", 17 | "diablo2", 18 | "packets" 19 | ], 20 | "author": "Louis Beaumont", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/MephisTools/AutoTathamet/issues" 24 | }, 25 | "homepage": "https://github.com/MephisTools/AutoTathamet#readme", 26 | "dependencies": { 27 | "a-star": "^0.2.0", 28 | "diablo2-data": "^1.2.0", 29 | "diablo2-protocol": "^1.3.2", 30 | "ws": "^6.1.3" 31 | }, 32 | "devDependencies": { 33 | "argparse": "^1.0.10", 34 | "standard": "^12.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/plugins/players.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | bot.playerList = [] 3 | bot._client.on('D2GS_PLAYERJOINED', ({ playerId, charName }) => { 4 | bot.playerList.push({ id: playerId, name: Buffer.from(charName).toString().replace(/\0.*$/g, '') }) 5 | bot.say(`Welcome ${bot.playerList[bot.playerList.length - 1].name} id ${bot.playerList[bot.playerList.length - 1].id}`) 6 | }) 7 | 8 | bot._client.on('D2GS_PLAYERLEFT', (playerId) => { 9 | // If the master leave, we leave the game too 10 | if (bot.master !== null && bot.master.id === playerId) { 11 | bot.exit() 12 | } 13 | const index = bot.playerList.findIndex(e => e.playerId === playerId) 14 | bot.playerList.splice(index, 1) 15 | }) 16 | /* 17 | // Doesnt work yet 18 | bot._client.on('D2GS_PLAYERRELATIONSHIP', ({ unitId, state }) => { // AutoParty 19 | bot._client.write('D2GS_PARTY', { 20 | actionId: 7, // TODO: what is it 21 | playerId: unitId 22 | }) 23 | }) 24 | */ 25 | } 26 | 27 | module.exports = inject 28 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | function getRandomInt (min, max) { 2 | min = Math.ceil(min) 3 | max = Math.floor(max) 4 | return Math.floor(Math.random() * (max - min + 1)) + min 5 | } 6 | 7 | function degreeBetweenTwoPoints (a, b) { 8 | return Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI 9 | } 10 | 11 | // Return the coordinate of a point with at a distance dist and degree angle from point point 12 | function coordFromDistanceAndAngle (point, dist, angle) { 13 | return { x: Math.cos(angle * Math.PI / 180) * dist + point.x, y: Math.sin(angle * Math.PI / 180) * dist + point.y } 14 | } 15 | 16 | function distance (a, b) { 17 | return Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y)) 18 | } 19 | 20 | function squaredDistance (a, b) { 21 | return (b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y) 22 | } 23 | 24 | function delay (time) { 25 | return new Promise((resolve, reject) => { 26 | setTimeout(() => { 27 | resolve() 28 | }, time) 29 | }) 30 | } 31 | 32 | module.exports = { getRandomInt, degreeBetweenTwoPoints, coordFromDistanceAndAngle, distance, delay, squaredDistance } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Louis Beaumont 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/map.js: -------------------------------------------------------------------------------- 1 | class Map { 2 | constructor (cellSize = 20) { 3 | this.data = {} 4 | this.cellSize = cellSize 5 | } 6 | 7 | coordsToCell ({ x, y }) { 8 | return { x: Math.floor(x / this.cellSize), y: Math.floor(y / this.cellSize) } 9 | } 10 | 11 | static hashCell ({ x, y }) { 12 | return x + ',' + y 13 | } 14 | 15 | cellToCoord ({ x, y }) { 16 | // middle of the cell 17 | return { x: (x + 0.5) * this.cellSize, y: (y + 0.5) * this.cellSize } 18 | } 19 | 20 | isPositionWalkable (c) { 21 | const v = this.getAtPosition(c) 22 | return v === undefined || v === false 23 | } 24 | 25 | isCellWalkable (c) { 26 | const v = this.getAtCell(c) 27 | return v === undefined || v === false 28 | } 29 | 30 | // map is a grid of 20x20 cells 31 | // undefined : unknown 32 | // true : wall 33 | // false : ground 34 | getAtPosition (p) { 35 | return this.getAtCell(this.coordsToCell(p)) 36 | } 37 | 38 | setAtPosition (p, isWall) { 39 | this.setAtCell(this.coordsToCell(p), isWall) 40 | } 41 | 42 | getAtCell (c) { 43 | return this.data[Map.hashCell(c)] 44 | } 45 | 46 | setAtCell (c, isWall) { 47 | this.data[Map.hashCell(c)] = isWall 48 | } 49 | } 50 | 51 | module.exports = Map 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoTathamet 2 | [![NPM version](https://img.shields.io/npm/v/autotathamet.svg)](http://npmjs.com/package/autotathamet) 3 | [![Build Status](https://img.shields.io/circleci/project/MephisTools/AutoTathamet/master.svg)](https://circleci.com/gh/MephisTools/AutoTathamet) 4 | [![Discord Chat](https://img.shields.io/badge/discord-here-blue.svg)](https://discord.gg/9RqtApv) 5 | [![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/MephisTools/AutoTathamet) 6 | 7 | Create Diablo2 bots with a powerful, stable, and high level JavaScript API. 8 | 9 | 10 | 11 | ## Features 12 | 13 | * chat 14 | * follow 15 | * pick up items 16 | 17 | ## Roadmap 18 | 19 | * map 20 | * inventory 21 | 22 | ## Usage 23 | 24 | ```js 25 | const { createBot } = require('autotathamet') 26 | 27 | async function start () { 28 | const bot = await createBot({ 29 | host: 'battlenetIp', 30 | username: 'myUser', 31 | password: 'myPassword', 32 | version: '1.14', 33 | keyClassic: 'my16CharsKey', 34 | keyExtension: 'my16CharsKey' 35 | }) 36 | await bot.selectCharacter('mycharacter') 37 | await bot.createGame('mygame', '', 'game server', 0) 38 | } 39 | 40 | start() 41 | 42 | ``` 43 | 44 | ## Documentation 45 | 46 | * See docs/API.md 47 | -------------------------------------------------------------------------------- /lib/pathFinding.js: -------------------------------------------------------------------------------- 1 | const astar = require('a-star') 2 | const Map = require('./map') 3 | const { squaredDistance } = require('./utils') 4 | 5 | function walkNeighborsCandidates ({ x, y }) { 6 | return [ 7 | { x: x + 1, y: y }, 8 | { x: x, y: y + 1 }, 9 | { x: x + 1, y: y + 1 }, 10 | { x: x - 1, y: y }, 11 | { x: x - 1, y: y - 1 }, 12 | { x: x, y: y - 1 }, 13 | { x: x - 1, y: y + 1 }, 14 | { x: x + 1, y: y - 1 } 15 | ] 16 | } 17 | 18 | function tpNeighborsCandidates ({ x, y }) { 19 | const candidates = [] 20 | for (let xc = x - 3; xc < x + 3; xc++) { 21 | for (let yc = y - 3; yc < y + 3; yc++) { 22 | candidates.push({ x: xc, y: yc }) 23 | } 24 | } 25 | return candidates 26 | } 27 | 28 | function findPath (startPosition, destination, map, neighborsCandidates = walkNeighborsCandidates, timeout = 1000) { 29 | const startCell = map.coordsToCell(startPosition) 30 | const destinationCell = map.coordsToCell(destination) 31 | const cellsPath = astar({ 32 | start: startCell, 33 | isEnd: c => squaredDistance(c, destinationCell) === 0, 34 | neighbor: (p) => neighborsCandidates(p).filter(c => map.isCellWalkable(c)), 35 | distance: (a, b) => squaredDistance(a, b), 36 | heuristic: (c) => squaredDistance(c, destinationCell), 37 | hash: c => Map.hashCell(c), 38 | timeout 39 | }) 40 | if (cellsPath === null) { 41 | return null 42 | } 43 | const { status, cost, path } = cellsPath 44 | return { status, cost, path: path.map(c => map.cellToCoord(c)) } 45 | } 46 | 47 | module.exports = { findPath, walkNeighborsCandidates, tpNeighborsCandidates } 48 | -------------------------------------------------------------------------------- /examples/bot.js: -------------------------------------------------------------------------------- 1 | const { createBot } = require('../index') 2 | 3 | var ArgumentParser = require('argparse').ArgumentParser 4 | var parser = new ArgumentParser({ 5 | version: '1.4.1', 6 | addHelp: true, 7 | description: 'Simple bot' 8 | }) 9 | parser.addArgument([ '-au', '--username' ], { required: true }) 10 | parser.addArgument([ '-ap', '--password' ], { required: true }) 11 | parser.addArgument([ '-c', '--character' ], { required: true }) 12 | parser.addArgument([ '-gn', '--gameName' ], { required: true }) 13 | parser.addArgument([ '-gp', '--gamePassword' ], { required: true }) 14 | parser.addArgument([ '-gs', '--gameServer' ], { required: true }) 15 | parser.addArgument([ '-s', '--sidServer' ], { required: true }) 16 | parser.addArgument([ '-dv', '--diabloVersion' ], { defaultValue: '1.14' }) // How do we use version.js from diablo2-protocol ? 17 | parser.addArgument([ '-k1', '--keyClassic' ], { required: true }) 18 | parser.addArgument([ '-k2', '--keyExtension' ], { required: true }) 19 | parser.addArgument([ '-dp', '--delayPackets' ], { defaultValue: 500 }) // Only servers with anti hack system should use delay between packets 20 | 21 | const { username, password, character, gameName, gamePassword, gameServer, sidServer, diabloVersion, keyClassic, keyExtension, delayPackets } = parser.parseArgs() 22 | 23 | async function start () { 24 | const bot = await createBot({ 25 | host: sidServer, 26 | username, 27 | password, 28 | version: diabloVersion, 29 | keyClassic, 30 | keyExtension, 31 | delayPackets 32 | }) 33 | await bot.selectCharacter(character) 34 | await bot.createGame(gameName, gamePassword, gameServer, 0) 35 | } 36 | 37 | start() 38 | -------------------------------------------------------------------------------- /lib/plugins/skills.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | bot.castSkillOnLocation = async (x, y, skill) => { 3 | bot.say(`Casting ${skill} at ${x}:${y}`) 4 | await bot.switchSkill(0, skill) // Why would we use left hand ? 5 | bot._client.write('D2GS_RIGHTSKILLONLOCATION', { 6 | x: parseInt(x), 7 | y: parseInt(y) 8 | }) 9 | } 10 | 11 | bot.switchSkill = (hand, skill) => { 12 | return new Promise(resolve => { 13 | const callback = () => { 14 | resolve(false) 15 | } 16 | const timeOut = setTimeout(() => { // SETSKILL doesn't proc means ain't got this skill 17 | bot._client.removeListener('D2GS_SETSKILL', callback) 18 | bot.say(`I don't have skill ${skill}`) 19 | }, 2000) 20 | bot._client.on('D2GS_SETSKILL', callback) 21 | if (hand === 0) { 22 | if (bot.rightHand !== skill) { 23 | bot._client.write('D2GS_SWITCHSKILL', { 24 | skill: skill, 25 | unk1: 0, 26 | hand: 0, // 0 = right, 128 = left 27 | unknown: [255, 255, 255, 255] 28 | }) 29 | bot.rightHand = skill 30 | } 31 | } else { 32 | if (bot.leftHand !== skill) { 33 | bot._client.write('D2GS_SWITCHSKILL', { 34 | skill: skill, 35 | unk1: 0, 36 | hand: 128, // 0 = right, 128 = left 37 | unknown: [255, 255, 255, 255] 38 | }) 39 | bot.leftHand = skill 40 | } 41 | } 42 | clearTimeout(timeOut) 43 | resolve(true) 44 | }) 45 | } 46 | 47 | bot.castSkillOnEntity = (type, id, skill) => { 48 | bot.say(`Casting ${skill} on ${id}`) 49 | bot.switchSkill(0, skill) 50 | bot._client.write('D2GS_RIGHTSKILLONENTITYEX3', { 51 | entityType: type, 52 | entityId: id 53 | }) 54 | } 55 | 56 | // Handle leveling up skills 57 | bot.autoSkill = () => { 58 | } 59 | } 60 | 61 | module.exports = inject 62 | -------------------------------------------------------------------------------- /pickitExample.json: -------------------------------------------------------------------------------- 1 | { 2 | "conditions": [ 3 | { "name": "ring", "quality": "unique", "properties": [ 4 | {"name": "itemmaxmanapercent", "value": "25", "operator": ">" } 5 | ] 6 | }, 7 | { "name": "flyingaxe", "quality": "unique", "ethereal": 0, "properties": [ 8 | {"name": "enhanceddamage", "value": "210", "operator": "=" } 9 | ] 10 | }, 11 | { "name": "fullrejuvenationpotion" }, 12 | { "name": "ataghan", "quality": "rare", "ethereal": 0, "properties": [ 13 | {"name": "ias", "value": "40", "operator": "=" }, 14 | {"name": "enhanceddamage", "value": "290", "operator": ">" }, 15 | {"name": "sockets", "value": "2", "operator": "=" } 16 | ] 17 | }, 18 | { "type": "hp1" }, 19 | { "type": "hp2" }, 20 | { "type": "hp3" }, 21 | { "type": "hp4" }, 22 | { "type": "hp5" }, 23 | { "type": "r01" }, 24 | { "type": "r02" }, 25 | { "type": "r03" }, 26 | { "type": "r04" }, 27 | { "type": "r05" }, 28 | { "type": "r06" }, 29 | { "type": "r07" }, 30 | { "type": "r08" }, 31 | { "type": "r09" }, 32 | { "type": "r10" }, 33 | { "type": "r11" }, 34 | { "type": "r12" }, 35 | { "type": "r13" }, 36 | { "type": "r14" }, 37 | { "type": "r15" }, 38 | { "type": "r16" }, 39 | { "type": "r17" }, 40 | { "type": "r18" }, 41 | { "type": "r19" }, 42 | { "type": "r20" }, 43 | { "type": "r21" }, 44 | { "type": "r22" }, 45 | { "type": "r23" }, 46 | { "type": "r24" }, 47 | { "type": "r25" }, 48 | { "type": "r26" }, 49 | { "type": "r27" }, 50 | { "type": "r28" }, 51 | { "type": "r29" }, 52 | { "type": "r30" }, 53 | { "type": "r31" }, 54 | { "type": "r32" }, 55 | { "type": "r33" } 56 | ] 57 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { createClientDiablo } = require('diablo2-protocol') 2 | const attack = require('./lib/plugins/attack') 3 | const chat = require('./lib/plugins/chat') 4 | const commands = require('./lib/plugins/commands') 5 | const exit = require('./lib/plugins/exit') 6 | const farm = require('./lib/plugins/farm') 7 | const inventory = require('./lib/plugins/inventory') 8 | const players = require('./lib/plugins/players') 9 | const position = require('./lib/plugins/position') 10 | const skills = require('./lib/plugins/skills') 11 | const stats = require('./lib/plugins/stats') 12 | const town = require('./lib/plugins/town') 13 | const websocketApi = require('./lib/plugins/websocketApi') 14 | const EventEmitter = require('events').EventEmitter 15 | 16 | class Bot extends EventEmitter { 17 | constructor () { 18 | super() 19 | this._client = null 20 | } 21 | 22 | async connect (options) { 23 | this._client = createClientDiablo(options) 24 | await this._client.connect() 25 | this.username = this._client.username 26 | this._client.on('connect', () => { 27 | this.emit('connect') 28 | }) 29 | this._client.on('error', (err) => { 30 | this.emit('error', err) 31 | }) 32 | this._client.on('end', () => { 33 | this.emit('end') 34 | }) 35 | this.selectCharacter = this._client.selectCharacter 36 | this.joinGame = this._client.joinGame 37 | this.createGame = this._client.createGame 38 | } 39 | 40 | end () { 41 | this._client.end() 42 | } 43 | } 44 | 45 | async function createBot (options) { 46 | const bot = new Bot() 47 | 48 | const p = bot.connect(options) 49 | attack(bot, options) 50 | chat(bot, options) 51 | commands(bot, options) 52 | exit(bot, options) 53 | farm(bot, options) 54 | inventory(bot, options) 55 | players(bot, options) 56 | position(bot, options) 57 | skills(bot, options) 58 | stats(bot, options) 59 | town(bot, options) 60 | websocketApi(bot, options) 61 | 62 | await p 63 | 64 | return bot 65 | } 66 | 67 | module.exports = { createBot } 68 | -------------------------------------------------------------------------------- /lib/plugins/stats.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | bot.life = 0 3 | bot.mana = 0 4 | bot.maxLife = 0 5 | bot.maxMana = 0 6 | bot._client.on('D2GS_LIFEANDMANAUPDATE', ({ life, mana, stamina, x, y, unknown }) => { 7 | // bot.say(`Life ${life}/${bot.maxLife}`) 8 | // bot.say(`Mana ${mana}/${bot.maxMana}`) 9 | // Tmp thing since we're dont have a packet saying what is my max life / mana 10 | if (life > bot.maxLife) { 11 | bot.maxLife = life 12 | } 13 | if (mana > bot.maxMana) { 14 | bot.maxMana = mana 15 | } 16 | 17 | bot.life = life 18 | bot.mana = mana 19 | bot.checkLifeMana() 20 | }) 21 | 22 | // Check if enough life / mana, take or not potion 23 | bot.checkLifeMana = () => { 24 | if (bot.life < bot.maxLife / 2) { // Treshold ? 25 | if (bot.usePotion('hp')) { 26 | bot.say('I used hp potion') 27 | } 28 | } 29 | if (bot.mana < bot.maxMana / 2) { 30 | if (bot.usePotion('mp')) { 31 | bot.say('I used mp potion') 32 | } 33 | } 34 | } 35 | 36 | // Handle leveling up stats 37 | bot.autoStat = () => { 38 | } 39 | 40 | // This should handle when the bot have to exit when under a treshold of life to avoid dying 41 | bot.chicken = (treshold) => { 42 | } 43 | 44 | // Return true if successfully used potion, false if not (prob out of potion ?) 45 | bot.usePotion = (type) => { 46 | // D2GS_USEITEM {"itemId":26,"x":4671,"y":4554} 47 | // D2GS_USEBELTITEM {"itemId":32,"unknown":0} 48 | const pot = bot.inventory.find(item => { return item['type'].includes(type) }) // Includes = any hp / mp 49 | if (pot === undefined) { 50 | return false 51 | } 52 | // Container 3 and 2 inventory and 136 ? maybe with PoD its different since inventory is bigger 53 | bot._client.write('D2GS_USEITEM', { 54 | itemId: pot.id, 55 | x: pot.x, 56 | y: pot.y 57 | }) 58 | // Container 32 == belt 59 | bot._client.write('D2GS_USEBELTITEM', { 60 | itemId: pot.id, 61 | unknown: 0 62 | }) 63 | return true 64 | } 65 | } 66 | module.exports = inject 67 | -------------------------------------------------------------------------------- /test/testPickit.js: -------------------------------------------------------------------------------- 1 | const inventory = require('../lib/plugins/inventory') 2 | const events = require('events') 3 | let bot = {} 4 | bot._client = new events.EventEmitter() 5 | inventory(bot) 6 | // console.log(bot.checkItem(JSON.parse('{"action":0,"category":22,"id":133,"equipped":0,"in_socket":0,"identified":1,"switched_in":0,"switched_out":0,"broken":0,"potion":0,"has_sockets":0,"in_store":1,"not_in_a_socket":0,"ear":0,"start_item":0,"simple_item":1,"ethereal":0,"personalised":0,"gambling":0,"rune_word":0,"version":101,"ground":true,"x":5887,"y":5064,"unspecified_directory":false,"type":"hp1","name":"Minor Healing Potion","width":"1","height":"1","throwable":"0","stackable":"0","usable":"1","is_armor":false,"is_weapon":false,"quality":2}'))) 7 | console.log(`Pick elrune ? ${bot.checkItem(JSON.parse('{"name": "elrune"}'))}`) 8 | console.log(bot.checkItem(JSON.parse('{"action":4,"category":39,"id":27,"equipped":0,"in_socket":0,"identified":1,"switched_in":0,"switched_out":0,"broken":0,"potion":0,"has_sockets":0,"in_store":0,"not_in_a_socket":0,"ear":0,"start_item":0,"simple_item":0,"ethereal":0,"personalised":0,"gambling":0,"rune_word":0,"version":101,"ground":false,"directory":0,"x":7,"y":6,"container":2,"unspecified_directory":false,"type":"rin","name":"Ring","width":"1","height":"1","throwable":"0","stackable":"0","usable":"0","is_armor":false,"is_weapon":false,"quality":13,"level":56,"has_graphic":1,"graphic":5,"has_colour":0,"amount":9,"properties":[{"stat":"pierce_idx","value":0},{"stat":"maximum_mana","value":89},{"stat":"slain_monsters_rest_in_peace","value":0},{"stat":"mana_per_time","value":1336342},{"stat":"skill_stamina_percent","value":0},{"stat":"enemy_fire_resistance_reduction","value":204}]}'))) 9 | console.log(`Pick ? ${bot.checkItem(JSON.parse('{"action":4,"category":20,"id":526,"equipped":0,"in_socket":0,"identified":1,"switched_in":0,"switched_out":0,"broken":0,"potion":0,"has_sockets":0,"in_store":0,"not_in_a_socket":0,"ear":0,"start_item":0,"simple_item":1,"ethereal":0,"personalised":0,"gambling":0,"rune_word":0,"version":101,"ground":false,"directory":0,"x":5,"y":7,"container":2,"unspecified_directory":false,"type":"r04","name":"Nef Rune","width":"1","height":"1","throwable":"0","stackable":"0","usable":"0","is_armor":false,"is_weapon":false,"quality":2}'))}`) 10 | -------------------------------------------------------------------------------- /test/testPathFinding.js: -------------------------------------------------------------------------------- 1 | const Map = require('../lib/map') 2 | const { findPath, walkNeighborsCandidates, tpNeighborsCandidates } = require('../lib/pathFinding') 3 | 4 | function addMaze (map, proba) { 5 | for (let x = 20; x < 980; x += 20) { 6 | for (let y = 20; y < 980; y += 20) { 7 | map.setAtPosition({ x, y }, Math.random() < proba) 8 | } 9 | } 10 | } 11 | 12 | function addMapBorder (map) { 13 | for (let x = -60; x <= 1060; x += 20) { 14 | map.setAtPosition({ x, y: -60 }, true) 15 | map.setAtPosition({ x, y: -40 }, true) 16 | map.setAtPosition({ x, y: -20 }, true) 17 | map.setAtPosition({ x, y: 0 }, true) 18 | map.setAtPosition({ x, y: 1000 }, true) 19 | map.setAtPosition({ x, y: 1020 }, true) 20 | map.setAtPosition({ x, y: 1040 }, true) 21 | map.setAtPosition({ x, y: 1060 }, true) 22 | } 23 | for (let y = -60; y <= 1060; y += 20) { 24 | map.setAtPosition({ y, x: -60 }, true) 25 | map.setAtPosition({ y, x: -40 }, true) 26 | map.setAtPosition({ y, x: -20 }, true) 27 | map.setAtPosition({ y, x: 0 }, true) 28 | map.setAtPosition({ y, x: 1000 }, true) 29 | map.setAtPosition({ y, x: 1020 }, true) 30 | map.setAtPosition({ y, x: 1040 }, true) 31 | map.setAtPosition({ y, x: 1060 }, true) 32 | } 33 | } 34 | 35 | function emptyMap (neighborCandidates) { 36 | const map = new Map() 37 | 38 | const path2 = findPath({ x: 20, y: 20 }, { x: 980, y: 980 }, map, neighborCandidates) 39 | 40 | console.log('empty', JSON.stringify(path2)) 41 | } 42 | 43 | function withMaze (neighborCandidates, name, proba) { 44 | const map = new Map() 45 | addMapBorder(map) 46 | // full map 47 | addMaze(map, proba) 48 | 49 | const path2 = findPath({ x: 20, y: 20 }, { x: 980, y: 980 }, map, neighborCandidates) 50 | 51 | console.log(name, JSON.stringify(path2)) 52 | } 53 | 54 | console.log('walk :') 55 | emptyMap(walkNeighborsCandidates) 56 | withMaze(walkNeighborsCandidates, 'easy', 0.1) 57 | withMaze(walkNeighborsCandidates, 'medium', 0.3) 58 | withMaze(walkNeighborsCandidates, 'hard', 0.5) 59 | withMaze(walkNeighborsCandidates, 'very hard', 0.7) 60 | withMaze(walkNeighborsCandidates, 'impossible', 1) 61 | console.log('') 62 | 63 | console.log('tp :') 64 | emptyMap(tpNeighborsCandidates) 65 | withMaze(tpNeighborsCandidates, 'easy', 0.1) 66 | withMaze(tpNeighborsCandidates, 'medium', 0.3) 67 | withMaze(tpNeighborsCandidates, 'hard', 0.5) 68 | withMaze(tpNeighborsCandidates, 'very hard', 0.7) 69 | withMaze(tpNeighborsCandidates, 'very very hard', 0.9) 70 | withMaze(tpNeighborsCandidates, 'very very very hard', 0.95) 71 | withMaze(tpNeighborsCandidates, 'impossible', 1) 72 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## autotathamet.createBot(options) 4 | 5 | Connect to diablo and returns a promise resolving to a `ClientDiablo` instance 6 | 7 | `options` is an object containing the properties : 8 | * username : username of the account 9 | * password : password of the account 10 | * host : sid host : diablo server host 11 | 12 | 13 | ## autotathamet.Bot 14 | 15 | ### gold 16 | 17 | integer 18 | 19 | current total gold (inventory + stash) 20 | 21 | ### inventory 22 | 23 | array 24 | 25 | current items (body + inventory + stash + belt) 26 | 27 | ### npcShop 28 | 29 | array 30 | 31 | current items in npcShop (cleared when cancelling npc) 32 | 33 | ### npcs 34 | 35 | array 36 | 37 | npcs data 38 | 39 | ### scrolls 40 | 41 | object 42 | 43 | mp: total amount of mp potions in bot.inventory 44 | 45 | hp: total amount of hp potions in bot.inventory 46 | 47 | ### tasks 48 | 49 | array 50 | 51 | indexed by area id contains object linking npc role to his name 52 | 53 | ### master 54 | 55 | object 56 | 57 | id: id of master player 58 | 59 | name: name of master player 60 | 61 | ### playerList 62 | 63 | array 64 | 65 | objects containing players data in current game 66 | 67 | ### warps 68 | 69 | array 70 | 71 | warps data encountered 72 | 73 | ### objects 74 | 75 | array 76 | 77 | objects encountered 78 | 79 | ### area 80 | 81 | integer 82 | 83 | current area id 84 | 85 | ### x 86 | 87 | integer 88 | 89 | current position x 90 | 91 | ### y 92 | 93 | integer 94 | 95 | current position y 96 | 97 | ### life 98 | 99 | integer 100 | 101 | current life 102 | 103 | ### maxLife 104 | 105 | integer 106 | 107 | maximum life 108 | 109 | ### mana 110 | 111 | integer 112 | 113 | current mana 114 | 115 | ### maxMana 116 | 117 | integer 118 | 119 | maximum mana 120 | 121 | ### rightHand 122 | 123 | integer 124 | 125 | current skill in right hand 126 | 127 | ### leftHand 128 | 129 | integer 130 | 131 | current skill in left hand 132 | 133 | ### selectCharacter(character) 134 | 135 | select a character and returns a promise 136 | 137 | ### createGame(gameName, gamePassword, gameServer, difficulty) 138 | 139 | create and join the specified game and returns a promise 140 | 141 | 142 | ### say(message) 143 | 144 | says `message` 145 | 146 | ### pickupItems() 147 | 148 | starts picking up close by items 149 | 150 | ### run(x, y) 151 | 152 | run to this position 153 | 154 | ### runToWrap() 155 | 156 | returns the closest wrap 157 | 158 | ## castSkillOnLocation(x, y, skill) 159 | 160 | cast given skill on this location 161 | 162 | ### playerList 163 | 164 | contains the player list 165 | 166 | ### bot._client 167 | 168 | bot._client is the low level API and should be avoided if possible 169 | 170 | #### bot._client.on("packetName", params) 171 | 172 | for each diablo2 packet (see data/d2gs.json, data/mcp.json, data/bnftp.json, data/sid.json) 173 | emit an event when a packet is received 174 | 175 | #### bot._client.on("packet", name, params) 176 | 177 | emit an event with `name` and `params` 178 | 179 | #### bot._client.write(name, params) 180 | 181 | sends the packet `name` with `params` to the corresponding server 182 | -------------------------------------------------------------------------------- /lib/plugins/farm.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | function randomString () { 3 | const possible = 'abcdefghijklmnopqrstuvwxyz' 4 | let randomString = '' 5 | 6 | for (let i = 0; i < 5; i++) { randomString += possible.charAt(Math.floor(Math.random() * possible.length)) } 7 | return randomString 8 | } 9 | 10 | // This is supposed to farm one or multiple scripts like killing mephisto, doing a quest, cow level, rushing a mule ... in a loop 11 | bot.farmLoop = async (scriptToFarm, gameServer) => { 12 | let nbRuns = 100 13 | while (nbRuns > 0) { 14 | await bot.createGame(randomString(), randomString(), gameServer, 0) 15 | bot.checkTown() 16 | bot.takeWaypoint() 17 | // Pathfinding here 18 | bot.exit() 19 | nbRuns-- 20 | } 21 | } 22 | // Work in progress, doesn't work 23 | bot.mephisto = async () => { 24 | const diablo2Data = require('diablo2-data')('pod_1.13d') 25 | // The goal of this function is to 26 | // Check before these steps: is there fucking travincal mobs at the middle (near red portal) 27 | // throwing hydra on us (we die trying to kill Mephisto), if yes => kill them (very often light / fire immune) / go to next script 28 | // 1 - Make the bot teleport in front of Mephisto 29 | // 2- Mephisto will come near the bot, under a treshold of distance, the bot teleport to the bottom one time 30 | // 3 - And repeat 2 until Mephisto is stuck at a point we can hit him with far range spell such as Lightning (Lightning surge in pod) 31 | // 4 - Teleport to the bottom (spot where we can hit Mephisto and he can't hit the bot) 32 | // 5 - Spam spell until he dies, pickup items, leave 33 | bot.moat = async () => { 34 | let mephisto 35 | bot.moveTo(true, 17563, 8072) 36 | bot._client.once('D2GS_ASSIGNNPC', (data) => { 37 | if (data['unitCode'] === diablo2Data.monstersByName['Mephisto']) { 38 | mephisto = data 39 | } 40 | }) 41 | const callback = (data) => { 42 | if (data['unitCode'] === diablo2Data.monstersByName['Mephisto']) { 43 | mephisto = data 44 | } 45 | } 46 | bot._client.on('D2GS_NPCMOVE', callback) 47 | bot._client.once('D2GS_REPORTKILL', (data) => { 48 | bot._client.removeListener('D2GS_NPCMOVE', callback) 49 | }) 50 | 51 | // Listen for the event. 52 | bot.on('closeEnoughToMe', () => { 53 | bot.moveTo(bot.x + 10, bot.y + 5) 54 | }) 55 | 56 | while (mephisto['x'] === 99999 && mephisto['y'] === 99999) { // TODO: hardcode wanted mephisto position 57 | if (/* distance({ x: bot.x, y: bot.y }, { x: mephisto['x'], y: mephisto['y'] }) < 10 */bot === 'standardfaispaschier') { 58 | bot.emit('closeEnoughToMe') 59 | } 60 | } 61 | // Ok we're ready to kill him 62 | return mephisto 63 | } 64 | 65 | bot.checkTown() 66 | bot.takeWaypoint(101) 67 | // bot.doPrecast(true) 68 | 69 | if (!bot.moveToNextArea(true)) { 70 | throw new Error('Failed to move to Durance Level 3') 71 | } 72 | 73 | bot.moveTo(17566, 8069) 74 | 75 | const mephisto = bot.moat() 76 | 77 | bot.kill(mephisto) 78 | 79 | // bot.pickItems() 80 | } 81 | } 82 | module.exports = inject 83 | -------------------------------------------------------------------------------- /lib/plugins/attack.js: -------------------------------------------------------------------------------- 1 | const Utils = require('../utils') 2 | 3 | function inject (bot) { 4 | bot.toKill = [] // List of mobs to kill 5 | bot.killEverything = false 6 | bot.attacking = false 7 | bot.clear = async () => { 8 | return new Promise(resolve => { 9 | resolve() 10 | }) 11 | } 12 | bot.killAllTargets = async () => { 13 | // return new Promise(resolve => { 14 | bot.attacking = true 15 | let skip = false 16 | const timeOut = setTimeout(() => { 17 | bot.say(`Skipping these mobs`) 18 | skip = true 19 | }, 20000) 20 | while (bot.toKill.length > 0) { // While there is mobs to kill or we skip them after a time 21 | bot.say(`Monsters left to kill ${bot.toKill.length}`) 22 | let closestMob = bot.toKill[0] 23 | /* 24 | for (let mob in bot.toKill) { // Let's find the closest mob 25 | let currentDistance = Utils.distance({ x: bot.x, y: bot.y }, { x: mob.x, y: mob.y }) 26 | if (closestMob === undefined || currentDistance < Utils.distance({ x: bot.x, y: bot.y }, { x: closestMob.x, y: closestMob.y })) { 27 | closestMob = mob 28 | } 29 | } 30 | */ 31 | if (closestMob === undefined || skip) { 32 | return 33 | } 34 | await bot.kill(closestMob) 35 | bot.say(`Next mob ${closestMob.id}`) 36 | // Once we found the closest mob, throw ability on it until it dies 37 | // bot.castSkill(unitId, type, 49) 38 | // await Utils.delay(10000) 39 | } 40 | // resolve() 41 | // }) 42 | bot.toKill = [] 43 | clearTimeout(timeOut) 44 | bot.attacking = false 45 | } 46 | 47 | bot.kill = async (mob) => { 48 | // If we can't kill the mob in few secs, skip 49 | let skip = false 50 | const timeOut = setTimeout(() => { // TODO: maybe skip condition would be if life didn't change overtime idk (for bosses ...) 51 | bot.say(`Skipping this mob`) 52 | bot.toKill.splice(bot.toKill.findIndex(m => { return m.id === mob.id }), 1) 53 | console.log(`toKill ${bot.toKill}`) 54 | skip = true 55 | }, 2000) 56 | while (bot.toKill.find(m => m.id === mob.id)) { // While this mob isn't dead 57 | await bot.castSkillOnLocation(mob.x, mob.y, 49) 58 | // bot.castSkillOnEntity(0, mob.id, 49) 59 | await Utils.delay(300) // If the delay is too short, your spell won't do any dmg (prob a server side protection ;)) 60 | if (skip) { 61 | return 62 | } 63 | } 64 | clearTimeout(timeOut) 65 | bot.say(`I killed ${mob.id}`) 66 | } 67 | 68 | bot.addTarget = (id, x, y) => { 69 | // If it's not already in the list 70 | if (!bot.toKill.find(mob => mob.id === id)) { 71 | bot.say(`New target to kill ${id}`) 72 | bot.toKill.push({ id: id, x: x, y: y }) 73 | if (!bot.attacking) { // Just one attack loop at once 74 | bot.killAllTargets() 75 | } 76 | } 77 | } 78 | 79 | bot.autoKill = () => { 80 | bot.killEverything = !bot.killEverything 81 | bot.say(`killEverything ${bot.killEverything ? 'on' : 'off'}`) 82 | if (!bot.killEverything) { 83 | bot._client.removeAllListeners('D2GS_NPCMOVE') 84 | bot._client.removeAllListeners('D2GS_NPCMOVETOTARGET') 85 | bot._client.removeAllListeners('D2GS_NPCATTACK') 86 | bot._client.removeAllListeners('D2GS_REPORTKILL') 87 | bot.toKill = [] // Clear the list 88 | } else { 89 | /* 90 | // TODO: Find a way to not add non-killable npc (either traders or other idk izual body ..) 91 | bot._client.on('D2GS_NPCMOVE', ({ unitId, type, x, y }) => { 92 | bot.addTarget(unitId, x, y) 93 | }) 94 | */ 95 | bot._client.on('D2GS_NPCMOVETOTARGET', ({ unitId, type, x, y }) => { 96 | bot.addTarget(unitId, x, y) 97 | }) 98 | // Atm only attack npc that attack 99 | bot._client.on('D2GS_NPCATTACK', ({ unitId, attackType, targetId, targetType, x, y }) => { 100 | bot.addTarget(unitId, x, y) 101 | }) 102 | bot._client.on('D2GS_REPORTKILL', ({ unitId, type, x, y }) => { 103 | bot.toKill.splice(bot.toKill.findIndex(mob => { return mob.id === unitId }), 1) 104 | }) 105 | } 106 | } 107 | } 108 | 109 | module.exports = inject 110 | -------------------------------------------------------------------------------- /lib/plugins/commands.js: -------------------------------------------------------------------------------- 1 | function inject (bot) { 2 | bot.master = null 3 | bot.follow = false 4 | 5 | bot._client.on('D2GS_GAMECHAT', ({ charName, message }) => { 6 | if (message === '.master') { 7 | if (bot.master === null) { 8 | bot.say(`${charName} is now master`) 9 | try { 10 | bot.master = { id: bot.playerList.find(player => { return player.name === charName }).id, name: charName } 11 | bot._client.write('D2GS_PARTY', { 12 | actionId: 6, // TODO: what is it ? 6 invite, 7 cancel ? 13 | playerId: bot.playerList.find(player => { return player.name === charName }).id 14 | }) 15 | } catch (error) { 16 | console.log(error) 17 | bot.say(`I don't have his id !`) 18 | } 19 | } else { 20 | bot.say(`${bot.master} is already master`) 21 | } 22 | } 23 | 24 | if (bot.master !== null) { // Just a security 25 | if ((message === '.follow' || message === '.followpf' || message === '.followpftp') && charName === bot.master.name) { 26 | const doPf = message === '.followpf' 27 | const doPfTp = message === '.followpftp' 28 | 29 | bot.follow = !bot.follow 30 | bot.say(bot.follow ? `Following ${bot.master.name}` : `Stopped following ${bot.master.name}`) 31 | 32 | if (!bot.follow) { // TODO: when turning follow off maybe make the bots to go to portal spot in master act (if in camp) 33 | bot._client.removeAllListeners('D2GS_PLAYERMOVE') 34 | bot._client.removeAllListeners('D2GS_PORTALOWNERSHIP') 35 | bot._client.removeAllListeners('D2GS_CHARTOOBJ') 36 | } else { 37 | bot._client.on('D2GS_PLAYERMOVE', ({ targetX, targetY, unitId }) => { 38 | if (unitId === bot.master.id) { 39 | if (doPf) { 40 | bot.moveTo(false, targetX, targetY) 41 | } else if (doPfTp) { 42 | bot.moveTo(true, targetX, targetY) 43 | } else { 44 | bot.run(targetX, targetY) // Maybe use currentX, Y ? and runtoentity ? 45 | } 46 | } 47 | }) 48 | // Master opens a portal 49 | bot._client.on('D2GS_PORTALOWNERSHIP', ({ ownerId, ownerName, localId, remoteId }) => { // TODO: Why is this looping ? 50 | // bot.say(`${Buffer.from(ownerName).toString().replace(/\0.*$/g, '')}:${ownerId}:masterId:${bot.master.id} opened a portal close to me`) 51 | // bot.say(bot.master.id === ownerId ? `He is my master, incoming` : `He isn't my master i stay here !`) 52 | if (bot.master.id === ownerId) { 53 | bot._client.write('D2GS_RUNTOENTITY', { 54 | entityType: 2, 55 | entityId: localId 56 | }) 57 | bot._client.write('D2GS_INTERACTWITHENTITY', { 58 | entityType: 2, 59 | entityId: localId 60 | }) 61 | } 62 | }) 63 | // Master enter a warp 64 | bot._client.on('D2GS_CHARTOOBJ', ({ unknown, playerId, movementType, destinationType, objectId, x, y }) => { 65 | if (bot.master.id === playerId) { 66 | bot._client.write('D2GS_RUNTOENTITY', { 67 | entityType: destinationType, 68 | entityId: objectId 69 | }) 70 | bot._client.write('D2GS_INTERACTWITHENTITY', { 71 | entityType: destinationType, 72 | entityId: objectId 73 | }) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | // .town makes the bot go to town 80 | if (bot.master !== null && charName === bot.master.name && message === '.town') { 81 | bot.base() 82 | } 83 | 84 | if (message === '.autokill' && charName === bot.master.name) { 85 | bot.autoKill() 86 | } 87 | 88 | if (message === '.taketp' && charName === bot.master.name) { 89 | bot.takeMasterTp() 90 | } 91 | 92 | if (message === '.findWarp' && charName === bot.master.name) { 93 | bot.findWarp(true) 94 | } 95 | 96 | if (message === '.pickup' && charName === bot.master.name) { 97 | bot.pickup = !bot.pickup 98 | bot.say(`pickup ${bot.pickup ? 'on' : 'off'}`) 99 | if (!bot.pickup) { 100 | bot._client.removeAllListeners('D2GS_ITEMACTIONWORLD') 101 | } else { 102 | bot.initializePickit() 103 | } 104 | } 105 | 106 | if (message.startsWith('.item') && charName === bot.master.name && message.split(' ').length > 1) { 107 | bot.hasItem(message.split(' ')[1]) 108 | } 109 | 110 | if (message.startsWith('.pot') && charName === bot.master.name && message.split(' ').length > 1) { 111 | bot.dropPot(message.split(' ')[1] === 'hp') 112 | } 113 | 114 | if (message.startsWith('.wp') && charName === bot.master.name && message.split(' ').length > 1) { 115 | bot.takeWaypoint(parseInt(message.split(' ')[1])) 116 | } 117 | 118 | // Debug stuff 119 | if (message.startsWith('.write') && charName === bot.master.name && message.split(' ').length > 1) { 120 | try { 121 | bot._client.write(message.split(' ')[1]) 122 | } catch (error) { 123 | console.log(error) 124 | } 125 | } 126 | 127 | if (message.startsWith('.do') && charName === bot.master.name && message.split(' ').length > 1) { 128 | switch (message.split(' ')[1]) { 129 | case '1': 130 | bot.moveToNextArea(true) 131 | break 132 | case '2': 133 | bot.runToWarp() 134 | break 135 | case '3': 136 | bot.moveTo(true, bot.npcs[0].x, bot.npcs[0].y) 137 | break 138 | case '4': 139 | bot.moveTo(false, bot.npcs[0].x, bot.npcs[0].y) 140 | break 141 | case '5': 142 | bot.moveTo(false, bot.x + parseInt(message.split(' ')[2]), bot.y + parseInt(message.split(' ')[3])) 143 | break 144 | case '55': 145 | bot.moveTo(true, bot.x + parseInt(message.split(' ')[2]), bot.y + parseInt(message.split(' ')[3])) 146 | break 147 | case '6': // come -> doesn't work from far away because position unknown 148 | bot._client.once('D2GS_PLAYERMOVE', ({ targetX, targetY, unitId }) => { 149 | if (unitId === bot.master.id) { 150 | bot.moveTo(false, targetX, targetY) 151 | } 152 | }) 153 | break 154 | case '7': 155 | const npc = bot.npcs.find(npc => parseInt(npc['unitId']) === parseInt(message.split(' ')[2])) 156 | bot.moveTo(false, npc.x, npc.y) 157 | break 158 | case '8': 159 | const object = bot.objects.find(npc => parseInt(npc['objectId']) === parseInt(message.split(' ')[2])) 160 | bot.moveTo(false, object.x, object.y) 161 | break 162 | case '9': // come -> doesn't work from far away because position unknown 163 | bot._client.once('D2GS_PLAYERMOVE', ({ targetX, targetY, unitId }) => { 164 | if (unitId === bot.master.id) { 165 | bot.moveTo(true, targetX, targetY) 166 | } 167 | }) 168 | break 169 | case '10': 170 | bot.say(`Space left in container ${message.split(' ')[2]}: ${bot.spaceLeft(parseInt(message.split(' ')[2]))}`) 171 | break 172 | case '11': 173 | bot.stash() 174 | break 175 | case '12': 176 | bot.checkTown() 177 | break 178 | case '13': 179 | console.log(bot.checkRepair()) 180 | console.log(bot.checkPotions()) 181 | console.log(bot.hasItem('tbk')) 182 | break 183 | } 184 | } 185 | 186 | if (message.startsWith('.move') && charName === bot.master.name && message.split(' ').length > 3) { 187 | bot.moveTo(message.split(' ')[1] === 'tp', bot.x + parseInt(message.split(' ')[2], 10), bot.y + parseInt(message.split(' ')[3], 10)) 188 | } 189 | 190 | if (message.startsWith('.npc') && charName === bot.master.name && message.split(' ').length > 1) { 191 | if (bot.area === undefined) { 192 | return 193 | } 194 | switch (message.split(' ')[1]) { 195 | case 'heal': 196 | bot.reachNpc(bot.tasks[bot.area].heal) 197 | break 198 | case 'repair': 199 | bot.reachNpc(bot.tasks[bot.area].repair) 200 | break 201 | case 'gamble': 202 | bot.reachNpc(bot.tasks[bot.area].gamble) 203 | break 204 | case 'shop': 205 | bot.reachNpc(bot.tasks[bot.area].shop) 206 | break 207 | } 208 | } 209 | } 210 | }) 211 | } 212 | 213 | module.exports = inject 214 | -------------------------------------------------------------------------------- /lib/plugins/town.js: -------------------------------------------------------------------------------- 1 | const diablo2Data = require('diablo2-data')('pod_1.13d') 2 | 3 | function inject (bot) { 4 | bot.npcShop = [] // Contains the items inside the npc shop 5 | bot.npcs = [] // Contains the informations about this game npcs 6 | bot.scrolls = {} 7 | const callbackShop = (data) => { 8 | bot.npcShop.push(data) 9 | } 10 | 11 | // Because npcs are differents every act 12 | // In diablo 2, all npcs have different roles in every acts 13 | // Maybe we could add it to node-diablo2-data 14 | bot.tasks = [] 15 | bot.tasks[1] = { heal: 'Akara', shop: 'Akara', gamble: 'Gheed', repair: 'Charsi', merc: 'Kashya', key: 'Akara', identify: 'Cain' } 16 | bot.tasks[40] = { heal: 'Fara', shop: 'Drognan', gamble: 'Elzix', repair: 'Fara', merc: 'Greiz', key: 'Lysander', identify: 'Cain' } 17 | bot.tasks[75] = { heal: 'Ormus', shop: 'Ormus', gamble: 'Alkor', repair: 'Hratli', merc: 'Asheara', key: 'Hratli', identify: 'Cain' } 18 | bot.tasks[103] = { heal: 'Jamella', shop: 'Jamella', gamble: 'Jamella', repair: 'Halbu', merc: 'Tyrael', key: 'Jamella', identify: 'Cain' } 19 | bot.tasks[109] = { heal: 'Malah', shop: 'Malah', gamble: 'Anya', repair: 'Larzuk', merc: 'Qual-Kehk', key: 'Malah', identify: 'Cain' } 20 | 21 | // Maybe we could also listen to the npcmove stuff but honestly they don't go too far 22 | // We can reach them easy with runtoentity 23 | bot._client.on('D2GS_ASSIGNNPC', (data) => { 24 | // If we ain't already got the npc in the list 25 | if (!bot.npcs.find(npc => npc['unitCode'] === data['unitCode'])) { 26 | bot.npcs.push(data) 27 | bot.say(`Detected a new npc name:${diablo2Data.npcs[data['unitCode']]['name']},id:${data['unitId']},unitCode:${data['unitCode']},x:${data['x']},y:${data['y']}`) 28 | } 29 | }) 30 | 31 | // Update our current amount of portal / identify scrolls in our books 32 | bot._client.on('D2GS_UPDATEITEMSKILL', (data) => { 33 | if (data['skill'] === 218) { 34 | bot.scrolls.identify = data['amount'] 35 | } 36 | if (data['skill'] === 220) { 37 | bot.scrolls.portal = data['amount'] 38 | } 39 | }) 40 | 41 | // bot.gold is total gold (inventory + stash) 42 | // bot.goldInInventory is gold in inventory (can be useful to know if we want the bot to put gold in stash to avoid losing it sometimes) 43 | bot._client.on('D2GS_SETDWORDATTR', (data) => { 44 | if (data['attribute'] === 15) { 45 | bot.gold = data['amount'] + bot.goldInInventory 46 | } 47 | if (data['attribute'] === 14) { 48 | bot.goldInInventory = data['amount'] 49 | } 50 | }) 51 | bot._client.on('D2GS_SETBYTEATTR', (data) => { 52 | if (data['attribute'] === 15) { 53 | bot.gold = data['amount'] + bot.goldInInventory 54 | } 55 | if (data['attribute'] === 14) { 56 | bot.goldInInventory = data['amount'] 57 | } 58 | }) 59 | 60 | // This method will check if i have to to go npc trader / healer / repairer / hire / gambler 61 | // If yes go to npc and buy stuffs 62 | // Atm we dont care about identify scrolls 63 | bot.checkTown = async () => { 64 | // if (bot.spaceLeft(2) < 40) { 65 | // await bot.stash() 66 | // } 67 | const potions = bot.checkPotions() 68 | const tome = bot.hasItem('tbk') // ibk is identify book 69 | const repair = bot.checkRepair() 70 | const merc = bot.checkMerc() // TODO: handle merc (not urgent) 71 | console.log(potions, tome, bot.scrolls.portal, repair, merc, bot.gold) 72 | 73 | if ((bot.checkHeal() || potions.hp < 0 || potions.mp < 0 || (!tome || bot.scrolls.portal < 21)) && bot.gold > 10000) { 74 | const npcId = await bot.reachNpc(bot.tasks[bot.area].shop) 75 | await bot.tradeNpc(npcId) 76 | // Buy stuff 77 | while (bot.checkPotions().hp) { 78 | await bot.buyItem('hp') 79 | } 80 | while (bot.checkPotions().mp) { 81 | await bot.buyItem('mp') 82 | } 83 | await bot.buyPotions(npcId, potions.hp, potions.mp) 84 | if (!tome) { 85 | await bot.buyItem('tbk') 86 | } 87 | while (bot.scrolls.portal < 21) { // isc is identify 88 | await bot.buyItem('tsc') 89 | } 90 | bot.cancelNpc(npcId) 91 | } 92 | 93 | if (repair.length > 0) { 94 | const npcId = await bot.reachNpc(bot.tasks[bot.area].repair) 95 | await bot.tradeNpc(npcId) 96 | // Repair 97 | await bot.repair(npcId, repair) 98 | bot.cancelNpc(npcId) 99 | } 100 | 101 | /* 102 | When buying scroll of portal (into a tome) 103 | D2GS_UPDATEITEMSKILL {"unknown":24,"unitId":1,"skill":220,"amount":16} 104 | 16 corresponds to the quantity of scrolls in the tome 105 | */ 106 | } 107 | 108 | // Init a npc, handle the case there is a talk, quest ... 109 | bot.initNpc = async (npcId) => { 110 | const callback = () => { 111 | bot._client.write('D2GS_NPCCANCEL', { 112 | entityType: 1, 113 | entityId: npcId 114 | }) 115 | bot._client.write('D2GS_INTERACTWITHENTITY', { 116 | entityType: 1, 117 | entityId: npcId 118 | }) 119 | bot._client.write('D2GS_NPCINIT', { 120 | entityType: 1, 121 | entityId: npcId 122 | }) 123 | } 124 | bot._client.once('D2GS_GAMEQUESTINFO', callback) 125 | bot._client.write('D2GS_INTERACTWITHENTITY', { 126 | entityType: 1, 127 | entityId: npcId 128 | }) 129 | bot._client.write('D2GS_NPCINIT', { 130 | entityType: 1, 131 | entityId: npcId 132 | }) 133 | bot._client.removeListener('D2GS_GAMEQUESTINFO', callback) 134 | } 135 | 136 | bot.tradeNpc = async (npcId) => { 137 | // Store the list of items of the npc 138 | bot._client.on('D2GS_ITEMACTIONWORLD', (callbackShop)) 139 | await bot.initNpc(npcId) 140 | bot._client.write('D2GS_NPCTRADE', { 141 | tradeType: 1, // TODO: what are the different types 142 | entityId: npcId, 143 | unknown: 0 144 | }) 145 | } 146 | 147 | bot.cancelNpc = (npcId) => { 148 | bot._client.write('D2GS_NPCCANCEL', { 149 | entityType: 1, 150 | entityId: npcId 151 | }) 152 | bot._client.removeListener('D2GS_ITEMACTIONWORLD', callbackShop) 153 | bot.npcShop = [] // Atm we don't care about storing npc shops after trading done, whats the point ? 154 | } 155 | 156 | bot.checkHeal = () => { 157 | if (bot.life < bot.maxLife || bot.mana < bot.maxMana) { 158 | return true 159 | } 160 | return false 161 | } 162 | 163 | // We will check if we have enough potions (according to the config) 164 | bot.checkPotions = () => { 165 | // For example we want to always have 4 health, 4 mana potions 166 | return { hp: 4 - bot.hasItems('hp'), mp: 4 - bot.hasItems('mp') } 167 | } 168 | 169 | bot.checkRepair = (treshold) => { 170 | let toRepair = [] 171 | 172 | bot.inventory.forEach(item => { 173 | // If the equipped item durability is under a treshold, this item has to be repaired 174 | if (item['equipped'] && item['durability'] < item['maximum_durability'] / 2) { 175 | toRepair.push(item) 176 | } 177 | }) 178 | 179 | return toRepair 180 | } 181 | 182 | bot.checkMerc = () => { 183 | } 184 | 185 | // Repair a list of items 186 | // Currently doing only repair all 187 | bot.repair = async (npcId, repairList) => { 188 | return new Promise(resolve => { 189 | bot._client.write('D2GS_REPAIR', { 190 | id1: npcId, 191 | id2: 0, // itemid 192 | id3: 0, // tab 193 | id4: 2147483648 // unknown ?? 194 | }) 195 | bot._client.once('D2GS_NPCTRANSACTION', (data) => { 196 | if (data['result'] === 2) { 197 | resolve(true) 198 | } else { 199 | resolve(false) 200 | } 201 | }) 202 | }) 203 | } 204 | 205 | // Buy one item to npcId 206 | bot.buyItem = async (npcId, type) => { 207 | return new Promise(resolve => { 208 | const callback = (data) => { 209 | bot.inventory.push(data) 210 | } 211 | bot._client.once('D2GS_ITEMACTIONWORLD', callback) 212 | bot._client.write('D2GS_NPCBUY', { 213 | npcId: npcId, 214 | itemId: bot.npcShop.find(item => { return item['type'].include(type) })['id'], 215 | bufferType: 0, 216 | cost: 0 217 | }) 218 | bot._client.once('D2GS_NPCTRANSACTION', ({ tradeType, result, unknown, merchandiseId, goldInInventory }) => { 219 | bot._client.removeListener('D2GS_ITEMACTIONWORLD') 220 | if (result === 0) { 221 | resolve(true) 222 | } else { 223 | console.log(`Failed transaction ${type}`) 224 | resolve(false) 225 | } 226 | }) 227 | }) 228 | } 229 | 230 | bot.identify = async (cain = true) => { 231 | if (cain) { 232 | await bot.reachNpc('Cain') 233 | // ??? 234 | } else { 235 | 236 | } 237 | } 238 | 239 | // TODO: fix reachNpc 240 | bot.reachNpc = async (npcName) => { 241 | try { 242 | console.log('npcindex', diablo2Data.npcs.findIndex(dnpc => dnpc['name'].includes(npcName))) 243 | bot.npcs.forEach(npc => { 244 | console.log('bot.npcsindex', npc['unitCode']) 245 | }) 246 | const npc = bot.npcs.find(npc => npc['unitCode'] === diablo2Data.npcs.findIndex(dnpc => dnpc['name'].includes(npcName))) // BROKEN SOMETIMES 247 | bot.say(`bot.reachNpc npc ${JSON.stringify(npc)}`) 248 | 249 | await bot.moveToEntity(false, npc['unitId']) 250 | return npc['unitId'] 251 | } catch (error) { 252 | console.log(`bot.reachNpc ${error}`) 253 | } 254 | } 255 | } 256 | 257 | module.exports = inject 258 | -------------------------------------------------------------------------------- /lib/plugins/dumps.md: -------------------------------------------------------------------------------- 1 | # Some dumps of packets useful for writing plugins 2 | 3 | ## Received when joining game 4 | Could be nice to list every packets and use every informations received at loading 5 | 6 | ### Stats 7 | >received compressed packet D2GS_ATTRIBUTEUPDATE {"unitId":1,"attribute":67,"amount":100} 8 | received compressed packet D2GS_ATTRIBUTEUPDATE {"unitId":1,"attribute":68,"amount":100} 9 | received compressed packet D2GS_ATTRIBUTEUPDATE {"unitId":1,"attribute":12,"amount":78} 10 | received compressed packet D2GS_ATTRIBUTEUPDATE {"unitId":1,"attribute":0,"amount":76} 11 | received compressed packet D2GS_ATTRIBUTEUPDATE {"unitId":1,"attribute":2,"amount":25} 12 | received compressed packet D2GS_SETBYTEATTR {"attribute":12,"amount":93} 13 | received compressed packet D2GS_SETBYTEATTR {"attribute":0,"amount":93} 14 | received compressed packet D2GS_SETBYTEATTR {"attribute":2,"amount":25} 15 | received compressed packet D2GS_SETBYTEATTR {"attribute":0,"amount":93} 16 | received compressed packet D2GS_SETBYTEATTR {"attribute":1,"amount":125} 17 | received compressed packet D2GS_SETBYTEATTR {"attribute":2,"amount":25} 18 | received compressed packet D2GS_SETWORDATTR {"attribute":3,"amount":297} 19 | received compressed packet D2GS_SETDWORDATTR {"attribute":7,"amount":192512} 20 | received compressed packet D2GS_SETDWORDATTR {"attribute":9,"amount":102144} 21 | received compressed packet D2GS_SETDWORDATTR {"attribute":11,"amount":141568} 22 | received compressed packet D2GS_SETBYTEATTR {"attribute":12,"amount":93} 23 | received compressed packet D2GS_SETDWORDATTR {"attribute":15,"amount":1062774} 24 | 25 | 26 | ## Misc 27 | 28 | ### Portal opened near 29 | >received compressed packet D2GS_TOWNPORTALSTATE {"state":3,"areaId":83,"unitId":236} 30 | received compressed packet D2GS_PORTALOWNERSHIP {"ownerId":1,"ownerName":[99,104,101,97,112,0,0,0,22,37,2,236,0,0,0,124],"localId":236,"remoteId":235} 31 | received compressed packet D2GS_OBJECTSTATE {"unitType":2,"unitId":236,"unknown":3,"unitState":513} 32 | received compressed packet D2GS_GAMELOADING {} 33 | received compressed packet D2GS_TOWNPORTALSTATE {"state":3,"areaId":83,"unitId":236} 34 | 35 | **walking close to already opened portal** 36 | 37 | >received compressed packet D2GS_WORLDOBJECT {"objectType":2,"objectId":203,"objectUniqueCode":59,"x":5158,"y":5068,"state":2,"interactionCondition":83} 38 | received compressed packet D2GS_TOWNPORTALSTATE {"state":3,"areaId":83,"unitId":203} 39 | received compressed packet D2GS_PORTALOWNERSHIP {"ownerId":2,"ownerName":[99,104,101,97,112,0,0,0,22,39,2,203,0,0,0,124],"localId":203,"remoteId":202} 40 | 41 | ### Interact with stash 42 | 43 | >d2gsToServer : D2GS_RUNTOENTITY {"entityType":2,"entityId":17} 44 | d2gsToServer : D2GS_RUNTOENTITY {"entityType":2,"entityId":17} 45 | d2gsToServer : D2GS_INTERACTWITHENTITY {"entityType":2,"entityId":17} 46 | d2gsToClient : D2GS_TRADEACTION {"requestType":16} 47 | 48 | 49 | ### Above dump of case when npc has something to say to us (quest ...) 50 | 51 | >d2gsToServer : D2GS_TOWNFOLK {"unk1":1,"unk2":8,"unk3":4677,"unk4":6099} 52 | d2gsToServer : D2GS_INTERACTWITHENTITY {"entityType":1,"entityId":8} 53 | d2gsToClient : D2GS_NPCINFO {"unitType":1,"unitId":8,"unknown":[5,0,2,0,73,0,2,0,89,0,2,0,157,0,2,0,137,0,0,0,36,0,0,0,0,0,0,0,0,0,0,0,0,0]} 54 | d2gsToClient : D2GS_GAMEQUESTINFO {"unknown":[0,0,0,0,0,0,0,0,0,160,0,0,0,128,0,0,0,32,0,0,0,0,0,160,0,160,0,128,0,128,0,0,0,0,0,0,0,128,0,0,0,0,0,160,0,160,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,160,0,0,0,0,0,128,0,0,0,0,0,0,0,128,0,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0]} 55 | d2gsToClient : D2GS_QUESTINFO {"unknown":[1,8,0,0,0,0,1,0,12,0,8,0,8,0,25,144,20,0,25,16,1,0,1,0,0,0,1,16,5,16,129,17,5,16,37,16,1,0,0,0,0,0,1,16,0,0,0,0,9,16,1,18,1,0,0,0,4,0,1,16,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,33,16,0,0,8,0,0,0,9,16,85,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0]} 56 | d2gsToClient : D2GS_NPCSTOP {"unitId":8,"x":4678,"y":6100,"unitLife":128} 57 | d2gsToServer : D2GS_NPCINIT {"entityType":1,"entityId":8} 58 | d2gsToServer : D2GS_QUESTMESSAGE {"unk1":8,"unk2":36} 59 | d2gsToClient : D2GS_NPCMOVE {"unitId":9,"type":1,"x":4713,"y":6119,"unknown":5} 60 | d2gsToClient : D2GS_NPCSTOP {"unitId":6,"x":4682,"y":6155,"unitLife":128} 61 | d2gsToClient : D2GS_NPCMOVE {"unitId":9,"type":1,"x":4712,"y":6123,"unknown":5} 62 | d2gsToClient : D2GS_NPCMOVE {"unitId":5,"type":1,"x":4734,"y":6117,"unknown":5} 63 | d2gsToClient : D2GS_NPCSTOP {"unitId":8,"x":4678,"y":6100,"unitLife":128} 64 | d2gsToClient : D2GS_NPCSTOP {"unitId":6,"x":4682,"y":6155,"unitLife":128} 65 | d2gsToServer : D2GS_NPCCANCEL {"entityType":1,"npcId":8} // <=== Here i canceled his message to reinit 66 | d2gsToClient : D2GS_NPCSTOP {"unitId":8,"x":4678,"y":6100,"unitLife":128} 67 | d2gsToServer : D2GS_TOWNFOLK {"unk1":1,"unk2":8,"unk3":4677,"unk4":6099} 68 | d2gsToServer : D2GS_INTERACTWITHENTITY {"entityType":1,"entityId":8} 69 | d2gsToClient : D2GS_NPCINFO {"unitType":1,"unitId":8,"unknown":[4,0,2,0,73,0,2,0,89,0,2,0,157,0,2,0,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]} 70 | d2gsToClient : D2GS_GAMEQUESTINFO {"unknown":[0,0,0,0,0,0,0,0,0,160,0,0,0,128,0,0,0,32,0,0,0,0,0,160,0,160,0,128,0,128,0,0,0,0,0,0,0,128,0,0,0,0,0,160,0,160,0,0,0,0,0,0,0,128,0,0,0,0,0,0,0,0,0,0,0,160,0,0,0,0,0,128,0,0,0,0,0,0,0,128,0,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0]} 71 | d2gsToClient : D2GS_QUESTINFO {"unknown":[1,8,0,0,0,0,1,0,12,0,8,0,8,0,25,144,20,0,25,16,1,0,1,0,0,0,1,16,5,16,129,17,5,16,37,16,1,0,0,0,0,0,1,16,0,0,0,0,9,16,1,18,1,0,0,0,4,0,1,16,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,33,16,0,0,8,0,0,0,9,16,85,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0]} 72 | 73 | ### Identify item 74 | 75 | >d2gsToServer : D2GS_USEITEM {"itemId":163,"x":4679,"y":6098} 76 | d2gsToClient : D2GS_USESTACKABLEITEM {"unknown":[0,163,0,0,0,218,0]} 77 | d2gsToServer : D2GS_PING {"tickCount":7627312,"delay":45,"wardenResponse":0} 78 | d2gsToClient : D2GS_PONG {"tickCount":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]} 79 | d2gsToServer : D2GS_IDENTIFYITEM {"id1":32,"id2":163} 80 | d2gsToClient : D2GS_UPDATEITEMSTATS {"unknown":[7]} 81 | d2gsToClient : D2GS_UNUSED23 {} 82 | d2gsToClient : D2GS_WADDEXP {"amount":113} 83 | d2gsToClient : D2GS_GAMELOADING {} 84 | d2gsToClient : D2GS_UPDATEITEMSKILL {"unknown":152,"unitId":1,"skill":218,"amount":7} 85 | d2gsToClient : D2GS_USESCROLL {"type":4,"itemId":163} 86 | d2gsToClient : D2GS_USESTACKABLEITEM {"unknown":[255,163,0,0,0,255,255]} 87 | d2gsToClient : D2GS_ITEMACTIONOWNED {"action":21,"category":50,"id":32 ... 88 | d2gsToClient : D2GS_RELATOR1 {"param1":0,"unityId":1,"param2":0} 89 | d2gsToClient : D2GS_RELATOR2 {"param1":0,"unityId":1,"param2":0} 90 | d2gsToClient : D2GS_PLAYSOUND {"unitType":0,"unitId":1,"sound":6} 91 | 92 | 93 | ### Repair 94 | 95 | 96 | **When doing repair all** 97 | 98 | >d2gsToServer : D2GS_REPAIR {"id1":8,"id2":0,"id3":0,"id4":2147483648} 99 | d2gsToClient : D2GS_NPCSTOP {"unitId":6,"x":4682,"y":6155,"unitLife":128} 100 | d2gsToClient : D2GS_UPDATEITEMSTATS {"unknown":[7]} 101 | d2gsToClient : D2GS_MERCFORHIRE {"mercId":49442,"unknown":0} // <= TODO: wtf is mercforhire doing here when trading someone who repair ? 102 | 103 | **After receiving tons of GAMELOADING** 104 | 105 | d2gsToClient : D2GS_NPCTRANSACTION {"tradeType":1,"result":2,"unknown":155876362,"merchandiseId":4294967295,"goldInInventory":0} 106 | 107 | **TODO: is merchandiseId correct ? whats unknown ? (result 2 seems to be successful transaction)** 108 | 109 | d2gsToClient : D2GS_SETDWORDATTR {"attribute":15,"amount":346339} // TODO: what is saying ? 110 | 111 | 112 | 113 | **When doing repair 1 item (weapon in that case)** 114 | >d2gsToServer : D2GS_REPAIR {"id1":8,"id2":25,"id3":0,"id4":13} 115 | d2gsToClient : D2GS_UPDATEITEMSTATS {"unknown":[7]} 116 | d2gsToClient : D2GS_UNUSED8 {} 117 | d2gsToClient : D2GS_NPCTRANSACTION {"tradeType":1,"result":2,"unknown":155876362,"merchandiseId":4294967295,"goldInInventory":0} 118 | d2gsToClient : D2GS_SETDWORDATTR {"attribute":15,"amount":346204} 119 | 120 | 121 | ### When leaving a game 122 | 123 | >d2gsToServer : D2GS_GAMEEXIT {} 124 | d2gsToClient : D2GS_NPCSTOP {"unitId":9,"x":5758,"y":4541,"unitLife":128} 125 | d2gsToClient : D2GS_GAMECONNECTIONTERMINATED {} 126 | d2gsToClient : D2GS_UNLOADCOMPLETE {} 127 | d2gsToClient : D2GS_GAMEEXITSUCCESSFUL {} 128 | sidToServer : SID_LOGONREALMEX {"clientToken":9,"hashedRealmPassword":{"type":"Buffer","data":[32,230,46,20,250,152,165,84,159,7,128,137,51,19,23,208,85,125,135,189]},"realmTitle":"Path of Diablo"} 129 | sidToClient : SID_LOGONREALMEX {"MCPCookie":9,"MCPStatus":0,"MCPChunk1":[0,1656],"IP":[198,98,54,85],"port":6113,"zero":0,"MCPChunk2":[173112583,0,0,1144150096,13,0,0,4231807654,3473022855,1571197212,650787684,1325819087],"battleNetUniqueName":"Elfwallader"} 130 | Start of mcp session 131 | mcpToServer : Read error for size : undefined 132 | mcpToServer : MCP_STARTUP {"MCPCookie":9,"MCPStatus":0,"MCPChunk1":[0,1656],"MCPChunk2":[173112583,0,0,1144150096,13,0,0,4231807654,3473022855,1571197212,650787684,1325819087],"battleNetUniqueName":"Elfwallader"} 133 | mcpToClient : MCP_STARTUP {"result":0} 134 | mcpToServer : MCP_CHARLOGON {"characterName":"xzzad"} 135 | mcpToClient : MCP_CHARLOGON {"result":0} 136 | sidToServer : SID_GETCHANNELLIST {"productId":1144150096} 137 | sidToServer : SID_ENTERCHAT {"characterName":"xzzad","realm":"Path of Diablo,xzzad"} 138 | 139 | 140 | ### D2GS_REMOVEOBJECT 141 | 142 | >received compressed packet D2GS_REMOVEOBJECT {"unitType":2,"unitId":104} 143 | received compressed packet D2GS_REMOVEOBJECT {"unitType":2,"unitId":103} 144 | received compressed packet D2GS_REMOVEOBJECT {"unitType":2,"unitId":102} 145 | 146 | ### D2GS_WORLDOBJECT 147 | 148 | >d2gsToClient : D2GS_WORLDOBJECT {"objectType":2,"objectId":10,"objectUniqueCode":119,"x":4419,"y":5609,"state":2, 149 | "interactionCondition":0} 150 | 151 | ### D2GS_UPDATEITEMSKILL 152 | **when buying id scroll** 153 | 154 | >D2GS_UPDATEITEMSKILL {"unknown":152,"unitId":1,"skill":218,"amount":8} 155 | 156 | ### D2GS_USESCROLL 157 | **use scroll of portal (using skill 220)** 158 | 159 | received compressed packet D2GS_USESCROLL {"type":4,"itemId":225} 160 | -------------------------------------------------------------------------------- /lib/plugins/inventory.js: -------------------------------------------------------------------------------- 1 | const diablo2Data = require('diablo2-data')('pod_1.13d') 2 | const fs = require('fs') 3 | const path = require('path') 4 | function inject (bot) { 5 | // Idk what name should it has 6 | // When joining a game you get D2GS_ITEMACTIONOWNED for each items you have equipped, 7 | // D2GS_ITEMACTIONWORLD for each item in your inventory / stash 8 | // Save our items in arrays 9 | bot.inventory = [] 10 | bot.groundItems = [] 11 | bot.pickit = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../pickitExample.json'), 'utf8')) 12 | const itemCallback = (item) => { 13 | bot.inventory.push(item) 14 | } 15 | bot._client.on('D2GS_ITEMACTIONOWNED', itemCallback) 16 | bot._client.on('D2GS_ITEMACTIONWORLD', itemCallback) 17 | 18 | // We stop this behaviour after having saved all our inventory 19 | setTimeout(() => { 20 | bot._client.removeListener('D2GS_ITEMACTIONOWNED', itemCallback) 21 | bot._client.removeListener('D2GS_ITEMACTIONWORLD', itemCallback) 22 | }, 5000) 23 | 24 | bot.pickGroundItem = (item) => { 25 | bot._client.write('D2GS_RUNTOENTITY', { 26 | entityType: 4, 27 | entityId: item['id'] // 2nd element seems to be the id 28 | }) 29 | bot._client.write('D2GS_PICKUPITEM', { // Possible action IDs: 0x00 - Move item to inventory 0x01 - Move item to cursor buffer 30 | unitType: 4, 31 | unitId: item['id'] 32 | }) 33 | bot._client.once('D2GS_REMOVEOBJECT', ({ unitType, unitId }) => { // Maybe its not optimal ? (not sure it's me who picked it) 34 | bot.say(`Picked up ${item['name']}`) 35 | bot.checkLifeMana() // Eventually could do that only if it's a potion that has been picked up 36 | if (item['id'] === unitId) { 37 | bot.inventory.push(item) 38 | } 39 | const index = bot.groundItems.findIndex(item => { return item['id'] === unitId }) // Does this works ? 40 | if (index > -1) { 41 | bot.groundItems.splice(index, 1) 42 | } 43 | }) 44 | } 45 | 46 | // Returns: 47 | // -1 - Needs iding 48 | // 0 - Unwanted 49 | // 1 - NTIP wants 50 | // 2 - Cubing wants 51 | // 3 - Runeword wants 52 | // 4 - Pickup to sell (triggered when low on gold) 53 | bot.checkItem = (item) => { 54 | /* 55 | if (CraftingSystem.checkItem(unit)) { 56 | return { 57 | result: 5, 58 | line: null 59 | } 60 | } 61 | 62 | if (Cubing.checkItem(unit)) { 63 | return { 64 | result: 2, 65 | line: null 66 | } 67 | } 68 | 69 | if (Runewords.checkItem(unit)) { 70 | return { 71 | result: 3, 72 | line: null 73 | } 74 | } 75 | */ 76 | // TODO: handle cubing, runeword, craft (not urgent at all) 77 | let itemData 78 | if (item['is_armor']) { 79 | itemData = diablo2Data.armor.find(a => a['code'] === item['type']) 80 | } 81 | if (item['is_weapon']) { 82 | itemData = diablo2Data.weapons.find(w => w['code'] === item['type']) 83 | } 84 | // It means it's probably a misc item since it's not armor or weapon 85 | if (itemData === undefined) { 86 | itemData = diablo2Data.misc.find(m => m['code'] === item['type']) 87 | } 88 | // It means we didn't find any data about this item in the txt files (can occur when new item ...) 89 | if (itemData === undefined) { 90 | return false 91 | } 92 | for (let condition of bot.pickit['conditions']) { 93 | // If the item is in our condition of picking 94 | // Could we loop over all json properties and compare ? Would be a lot cleaner 95 | // console.log(item['name'], condition['name'], item['quality'], condition['quality'], item['ethereal'], condition['ethereal']) 96 | // Either we select an item over name, either over type 97 | if ((condition.hasOwnProperty('name') && item['name'].toLowerCase() === condition['name']) || 98 | (condition.hasOwnProperty('type') && item['type'] === condition['type']) 99 | /* && item['quality'] === condition['quality'] && item['ethereal'] === condition['ethereal'] */) { 100 | console.log('Name / type correspond') 101 | // If there is no properties condition, just name and basic stuff 102 | if (!condition.hasOwnProperty('properties')) { 103 | return true 104 | } 105 | let correspondingProperties = 0 106 | condition['properties'].forEach(conditionProperty => { 107 | item['properties'].forEach(itemProperty => { 108 | console.log(conditionProperty, itemProperty) 109 | // Means we find a corresponding property 110 | if (itemProperty['name'] === conditionProperty['name']) { 111 | console.log('Property correspond') 112 | // TODO: Find cleaner solution for operator 113 | // Checking if the found property is at our wanted value 114 | if (conditionProperty['operator'] === '=') { 115 | if (itemProperty['value'] === conditionProperty['value']) { 116 | correspondingProperties++ 117 | } 118 | } else if (conditionProperty['operator'] === '>') { 119 | if (itemProperty['value'] > conditionProperty['value']) { 120 | correspondingProperties++ 121 | } 122 | } else if (conditionProperty['operator'] === '<') { 123 | if (itemProperty['value'] < conditionProperty['value']) { 124 | correspondingProperties++ 125 | } 126 | } 127 | } 128 | }) 129 | }) 130 | // If we have the corresponding properties we wanted, return true 131 | if (condition['properties'].length === correspondingProperties) { 132 | // We also could imagine something like "more or less" these properties 133 | // So if an item drop which is close to our needs but not in our condition, would still pick it 134 | return true 135 | } 136 | } 137 | } 138 | 139 | // If total gold is less than 10k pick up anything worth 10 gold per square to sell in town. 140 | if (bot.gold < 10000) { 141 | // Gold doesn't take up room, just pick it up 142 | if (item['type'] === 'gld') { 143 | return true 144 | } 145 | 146 | if (itemData['cost'] / (item['width'] * item['height']) >= 10) { 147 | return true 148 | } 149 | } 150 | return false 151 | } 152 | 153 | // TODO: handle pickit 154 | bot.initializePickit = () => { 155 | if (bot.pickit === undefined) { 156 | bot.say(`No pickit setup - aborted`) 157 | return false 158 | } 159 | // Context: the bot is farming, an item fell on the ground 160 | bot._client.on('D2GS_ITEMACTIONWORLD', (item) => { 161 | if (!item['ground']) { 162 | return 163 | } 164 | bot.say(`${item['name']} has fallen`) 165 | bot.groundItems.push(item) 166 | // If yes take, remove from groundItems, check space, go drop stuff in stash if full 167 | if (bot.checkItem(item)) { 168 | bot.say(`I'm gonna pick ${item['name']}`) 169 | bot.pickGroundItem(item) 170 | } // Else, leave it on the ground, remove the item from groundItems after a few secs ? 171 | }) 172 | } 173 | 174 | // Drop a potion of health ? health : mana 175 | bot.dropPot = (health) => { 176 | try { 177 | const potion = bot.inventory.find(item => { return item['type'].includes(health ? 'hp' : 'mp') }) 178 | console.log('found potion', potion) 179 | // if (potion['container'] === 2) { // 2 = inventory 180 | bot._client.write('D2GS_DROPITEM', { 181 | itemId: potion['id'] 182 | }) 183 | // } 184 | // if (potion['container'] === 0) { // 0 = belt 185 | bot._client.write('D2GS_REMOVEBELTITEM', { 186 | itemId: potion['id'] 187 | }) 188 | // } 189 | } catch (error) { 190 | console.log(error) 191 | } 192 | } 193 | 194 | // Maybe for both these functions we should exclude stash ? Or add where param idk 195 | // Do i have this item ? (for example tbk is town book of portal) 196 | bot.hasItem = (type) => { 197 | return bot.inventory.find(item => item['type'].includes(type)) 198 | } 199 | 200 | // Return quantity of this item 201 | bot.hasItems = (type) => { 202 | let quantity = 0 203 | bot.inventory.forEach(item => { 204 | quantity += item['type'].includes(type) ? 1 : 0 205 | }) 206 | return quantity 207 | } 208 | 209 | // This should check my space left in the inventory 210 | bot.spaceLeft = (where) => { 211 | // Containers 212 | // 0 = body 213 | // 2 = inventory 214 | // 10 = stash 215 | // 32 = belt 216 | // (list in item.js) 217 | let space = 0 218 | if (where === 0) { 219 | } else if (where === 2) { 220 | space = 90 221 | } else if (where === 10) { 222 | space = 100 223 | } else if (where === 32) { 224 | // Retrieve our belt type and get his size in data 225 | let myBelt = bot.items.find(item => { return item['directory'] === 8 }) 226 | // Seems to doesn't count base belt size (addition) 227 | space = diablo2Data.belts.find(b => { return diablo2Data.armor.find(a => { return a['code'] === myBelt['type'] }) })['numboxes'] + 4 228 | } else if (where === 130) { 229 | space = 100 230 | } else if (where === 132) { 231 | space = 100 232 | } else if (where === 134) { 233 | space = 100 234 | } 235 | bot.inventory.forEach(item => { 236 | if (item['container'] === where) { 237 | space -= item['width'] * item['height'] 238 | } 239 | }) 240 | return space 241 | } 242 | 243 | // Put everything in the stash except the required stuff (set a parameter for things we wanna always keep such as book scroll, charms ...) 244 | // Put extra gold in stash ... 245 | bot.stash = async () => { 246 | // let stash = bot.objects.find(object => { return object['objectUniqueCode'] === diablo2Data.objectsByName['stash']['Id'] }) 247 | let stash 248 | for (let i = bot.objects.length - 1; i > 0; i--) { // We start at the end, just in case, so we check the latest received objects 249 | if (diablo2Data.objects[bot.objects[i]['objectUniqueCode']]['Name'].includes('bank')) { 250 | stash = bot.objects[i] 251 | } 252 | } 253 | if (stash === undefined) { // No stash in my area !!! 254 | bot.say(`No stash in area ${bot.area}`) 255 | return false 256 | } 257 | bot.moveToEntity(false, stash['objectId']) 258 | bot._client.write('D2GS_INTERACTWITHENTITY', { 259 | entityType: 2, 260 | entityId: stash['objectId'] 261 | }) 262 | bot._client.on('D2GS_TRADEACTION', (data) => { 263 | // Move stuff 264 | }) 265 | } 266 | } 267 | 268 | module.exports = inject 269 | -------------------------------------------------------------------------------- /lib/plugins/position.js: -------------------------------------------------------------------------------- 1 | const diablo2Data = require('diablo2-data')('pod_1.13d') 2 | const Utils = require('../utils') 3 | const Map = require('../map') 4 | const { findPath, walkNeighborsCandidates, tpNeighborsCandidates } = require('../pathFinding') 5 | 6 | function inject (bot) { 7 | bot.destination = null 8 | bot.warps = [] 9 | bot.objects = [] // This is used to store the objects around the bot 10 | bot.map = new Map(10) 11 | // Think about bot.objects list clearing ? delay ? or D2GS_REMOVEOBJECT ? 12 | // received compressed packet D2GS_REMOVEOBJECT {"unitType":2,"unitId":16} 13 | bot._client.on('D2GS_ASSIGNLVLWARP', (data) => { 14 | bot.warps.push(data) 15 | }) 16 | 17 | bot._client.on('D2GS_PORTALOWNERSHIP', ({ ownerId, ownerName, localId, remoteId }) => { 18 | bot.say(`${bot.playerList.find(p => { return p.id === ownerId }).name} opened a portal`) 19 | }) 20 | /* 21 | bot._client.on('D2GS_LOADACT', ({ areaId }) => { 22 | 23 | // received compressed packet D2GS_LOADACT {"act":0,"mapId":336199680,"areaId":5188,"unkwown":88448} 24 | // packet broken ????????????????????? 25 | 26 | if (bot.area !== areaId) { 27 | bot.say(`My area ${areaId}`) 28 | } 29 | bot.area = areaId 30 | }) 31 | */ 32 | bot._client.on('D2GS_MAPREVEAL', ({ areaId }) => { 33 | if (bot.area !== areaId) { 34 | bot.say(`My area ${areaId}`) 35 | } 36 | bot.area = areaId 37 | }) 38 | bot._client.on('D2GS_WORLDOBJECT', (object) => { 39 | // if (bot.objects.findIndex(ob => ob['objectId'] !== object['objectId'])) { // Don't duplicate the same object 40 | bot.say(`Detected worldobject ${diablo2Data.objects[object['objectUniqueCode']]['description - not loaded']}`) 41 | bot.objects.push(object) // Contains the objects around me 42 | // } 43 | }) 44 | bot._client.on('D2GS_REMOVEOBJECT', (object) => { 45 | if (bot.debug) { 46 | bot.say(`Removed worldobject ${diablo2Data.objects[object['objectUniqueCode']]['description - not loaded']}`) 47 | } 48 | // TODO: test this 49 | // bot.objects.splice(bot.objects.findIndex(ob => { return ob['unitId'] === object['unitId']}), 1) 50 | }) 51 | 52 | bot._client.on('D2GS_REASSIGNPLAYER', ({ x, y }) => { 53 | bot.x = x 54 | bot.y = y 55 | }) 56 | bot._client.on('D2GS_WALKVERIFY', ({ x, y }) => { 57 | bot.x = x 58 | bot.y = y 59 | }) 60 | 61 | bot.takeMasterTp = () => { 62 | // TODO: take portal of the master (need to know his area ... ?) 63 | } 64 | 65 | bot.moveToNextArea = async (teleportation) => { 66 | bot.say('Looking for the next level !') 67 | bot.say(await reachedWarp() ? 'Found the next level' : 'Could\'nt find the next level') 68 | } 69 | 70 | // Tentative to do pathfinding by exploring all 4 corners of the map 71 | // The bot should stop when receiving assignlvlwarp from the next area 72 | const DirectionsEnum = Object.freeze({ 'left': 1, 'top': 2, 'right': 3, 'bottom': 4 }) 73 | 74 | // This will return when the teleportation is done 75 | async function reachedPosition (previousPos) { 76 | return new Promise(resolve => { 77 | bot.say(`arrived at ${bot.x};${bot.y}`) 78 | bot.say(`previousPos difference ${Math.abs(bot.x - previousPos.x)};${Math.abs(bot.y - previousPos.y)}`) 79 | // We check if we moved 80 | resolve(Math.abs(bot.x - previousPos.x) > 5 || Math.abs(bot.y - previousPos.y) > 5) // Means we hit a corner 81 | }) 82 | } 83 | 84 | // TODO: Return the path used, to get optimized latter 85 | async function reachedWarp (direction = DirectionsEnum.left) { 86 | if (bot.warps.findIndex(warp => warp['warpId'] === bot.area + 1) !== -1) { // While we didn't go near the next level warp 87 | return true 88 | } 89 | // Reset the direction 90 | if (direction === DirectionsEnum.bottom) { 91 | direction = DirectionsEnum.left 92 | } 93 | let reachedCorner = false 94 | let nextPos = { x: bot.x, y: bot.y } 95 | while (!reachedCorner) { 96 | if (direction === DirectionsEnum.left) { 97 | nextPos = { x: bot.x, y: bot.y + 30 } 98 | } 99 | if (direction === DirectionsEnum.top) { 100 | nextPos = { x: bot.x + 30, y: bot.y } 101 | } 102 | if (direction === DirectionsEnum.right) { 103 | nextPos = { x: bot.x, y: bot.y - 30 } 104 | } 105 | if (direction === DirectionsEnum.top) { 106 | nextPos = { x: bot.x - 30, y: bot.y } 107 | } 108 | let previousPos = { x: bot.x, y: bot.y } 109 | await bot.moveTo(true, nextPos.x, nextPos.y) 110 | reachedCorner = await reachedPosition(previousPos) 111 | } 112 | bot.say(`Going ${direction}`) 113 | await reachedWarp(direction + 1) 114 | } 115 | 116 | function generatePosition (fromPos, d = 60) { 117 | const pos = { x: fromPos.x - d, y: fromPos.y - d } 118 | for (;pos.x < d + fromPos.x; pos.x += 20) { 119 | for (;pos.y < d + fromPos.y; pos.y += 20) { 120 | if (bot.map.getAtPosition(pos) === undefined) { 121 | return pos 122 | } 123 | } 124 | } 125 | return generatePosition(fromPos, d * 10) 126 | } 127 | 128 | bot.findWarp = async (teleportation) => { 129 | let done = false 130 | bot.on('D2GS_ASSIGNLVLWARP', () => { 131 | bot.say('I found a warp') 132 | done = true 133 | }) 134 | while (true) { 135 | const pos = generatePosition({ x: bot.x, y: bot.y }) 136 | console.log(pos) 137 | const r = await Promise.race([bot.moveTo(teleportation, pos.x, pos.y), Utils.delay(60000)]) 138 | if (done || r === false) { 139 | return 140 | } 141 | } 142 | } 143 | 144 | bot.moveTo = async (teleportation, x, y) => { 145 | if (x === undefined || y === undefined) { 146 | bot.say(`bot.moveTo incorrect coordinate undefined`) 147 | return false 148 | } 149 | await pf2(teleportation, x, y) 150 | } 151 | 152 | // TODO: test, make it works for all type of entity 153 | bot.moveToEntity = async (teleportation, entityId) => { 154 | let entity = bot.npcs.find(npc => { return npc['unitId'] === entityId }) 155 | let type = 1 156 | if (entity === undefined) { 157 | entity = bot.warps.find(warp => { return warp['unitId'] === entityId }) 158 | if (entity === undefined) { 159 | entity = bot.objects.find(object => { return object['objectId'] === entityId }) 160 | } 161 | type = 2 162 | } 163 | try { 164 | await pf2(teleportation, entity['x'], entity['y']) 165 | } catch (err) { 166 | bot.say('Oh sorry I crashed') 167 | console.log(err) 168 | } 169 | bot._client.write('D2GS_RUNTOENTITY', { 170 | entityType: type, // 1 seems to be npc, 2 portal ... 171 | entityId: entityId 172 | }) 173 | } 174 | 175 | async function pf2 (teleportation, x, y) { 176 | if (bot.destination !== null) { 177 | bot.destination = { x, y } 178 | await bot.pf2InternalPromise 179 | return 180 | } 181 | bot.destination = { x, y } 182 | bot.pf2InternalPromise = pf2Internal(teleportation, x, y) 183 | } 184 | async function pf2Internal (teleportation, x, y) { 185 | const verbose = true 186 | const verboseSay = (message) => { 187 | if (verbose) { 188 | bot.say(message) 189 | } 190 | } 191 | // const start = +new Date() 192 | let stuck = 0 193 | verboseSay(`My position ${bot.x} - ${bot.y}`) 194 | verboseSay(`Heading with astar to ${bot.destination.x} - ${bot.destination.y} by ${teleportation ? 'teleporting' : 'walking'}`) 195 | // We'll continue till arrived at destination 196 | let path = null 197 | let indexInPath = 0 198 | const lookForPath = () => { 199 | path = findPath({ x: bot.x, y: bot.y }, bot.destination, bot.map, teleportation ? tpNeighborsCandidates : walkNeighborsCandidates) 200 | if ((path.status !== 'success' && path.status !== 'timeout') || path.path.length < 2) { 201 | bot.wss.broadcast(JSON.stringify({ protocol: 'event', name: 'noPath' })) 202 | verboseSay('Sorry, I can\'t go there') 203 | console.log(path) 204 | bot.destination = null 205 | return false 206 | } else if (path.status === 'timeout') { 207 | if (path.path.length < 2) { 208 | bot.wss.broadcast(JSON.stringify({ protocol: 'event', name: 'noPath' })) 209 | verboseSay('Sorry, I can\'t go there') 210 | bot.destination = null 211 | return false 212 | } 213 | verboseSay('Searching the path took too long but I found a new path of cost ' + path.cost + ' and length ' + path.path.length + ', let\'s go !') 214 | } else { 215 | verboseSay('Found a new path of cost ' + path.cost + ' and length ' + path.path.length + ', let\'s go !') 216 | } 217 | bot.wss.broadcast(JSON.stringify({ protocol: 'event', name: 'path', params: path.path })) 218 | indexInPath = 1 219 | return true 220 | } 221 | while (Utils.distance({ x: bot.x, y: bot.y }, bot.destination) > 10.0) { 222 | const distance = Utils.distance({ x: bot.x, y: bot.y }, bot.destination) 223 | verboseSay(`Calculated distance ${distance.toFixed(2)}`) 224 | verboseSay(`Am i arrived ? ${distance <= 10.0}`) 225 | if (path === null || indexInPath >= path.path.length) { 226 | if (!lookForPath()) { 227 | return 228 | } 229 | } 230 | let dest = path.path[indexInPath] 231 | indexInPath++ 232 | verboseSay(`Movement from ${bot.x.toFixed(2)} - ${bot.y.toFixed(2)} to ${dest.x.toFixed(2)} - ${dest.y.toFixed(2)}`) 233 | const moved = await movementWithMapFilling(teleportation, dest.x, dest.y) 234 | if (!moved) { // If the bot is stuck 235 | verboseSay(`Stuck ${stuck} times`) 236 | stuck++ 237 | if (!lookForPath()) { 238 | return 239 | } 240 | } else { 241 | stuck = 0 242 | } 243 | } 244 | bot.destination = null 245 | bot.pf2InternalPromise = Promise.resolve() 246 | verboseSay(`Arrived at destination`) 247 | } 248 | 249 | async function movementWithMapFilling (teleportation, destX, destY) { 250 | const previousPosition = { x: bot.x, y: bot.y } 251 | await movement(teleportation, destX, destY) 252 | 253 | const currentPosition = { x: bot.x, y: bot.y } 254 | const dest = { x: destX, y: destY } 255 | if (Math.abs(previousPosition.x - currentPosition.x) < 2 && Math.abs(previousPosition.y - currentPosition.y) < 2) { // If the bot is stuck 256 | let obstacle 257 | if (teleportation) { 258 | obstacle = dest 259 | } else { 260 | obstacle = dest 261 | } 262 | console.log('stuck') 263 | bot.map.setAtPosition(obstacle, true) 264 | bot.wss.broadcast(JSON.stringify({ protocol: 'event', name: 'mapPoint', params: { x: obstacle.x, y: obstacle.y, isWall: true } })) 265 | // bot.say(`Obstacle at ${dest.x.toFixed(2)} - ${dest.y.toFixed(2)}`) 266 | return false 267 | } else { 268 | console.log('no stuck') 269 | const nature = bot.map.getAtPosition(dest) 270 | if (nature === undefined) { 271 | bot.wss.broadcast(JSON.stringify({ protocol: 'event', name: 'mapPoint', params: { x: dest.x, y: dest.y, isWall: false } })) 272 | bot.map.setAtPosition(dest, false) 273 | } 274 | } 275 | return true 276 | } 277 | bot.run = (x, y) => { 278 | bot._client.write('D2GS_RUNTOLOCATION', { 279 | x: x, 280 | y: y 281 | }) 282 | } 283 | 284 | // This will return when the movement is done 285 | async function movement (teleportation, destX, destY) { 286 | return new Promise(resolve => { 287 | if (!teleportation) { 288 | let timeOut 289 | const callbackWalkVerify = ({ x, y }) => { 290 | // bot.say(`endOfMovement at ${x};${y}`) 291 | bot.x = x 292 | bot.y = y 293 | clearTimeout(timeOut) 294 | resolve(true) 295 | } 296 | bot._client.once('D2GS_WALKVERIFY', callbackWalkVerify) 297 | bot.run(destX, destY) 298 | timeOut = setTimeout(() => { // in case we run in a wall 299 | // let's assume failure then 300 | bot._client.removeListener('D2GS_WALKVERIFY', callbackWalkVerify) 301 | resolve(false) 302 | }, 2000) 303 | } else { 304 | let timeOut 305 | const callback = ({ x, y }) => { 306 | // bot.say(`endOfMovement at ${x};${y}`) 307 | bot.x = x 308 | bot.y = y 309 | clearTimeout(timeOut) 310 | resolve(true) 311 | } 312 | bot.castSkillOnLocation(destX, destY, 53).then(() => { 313 | bot._client.once('D2GS_REASSIGNPLAYER', callback) 314 | }) 315 | 316 | timeOut = setTimeout(() => { // in case we run in a wall 317 | // let's assume failure then 318 | bot._client.removeListener('D2GS_REASSIGNPLAYER', callback) 319 | resolve(false) 320 | }, 2000) 321 | } 322 | }) 323 | } 324 | 325 | bot.runToWarp = () => { 326 | try { 327 | const nextArea = bot.warps.find(warp => { 328 | return warp['warpId'] === bot.area + 1 329 | }) 330 | bot.moveTo(false, nextArea.x, nextArea.y) 331 | bot.say(`Heading for the next area`) 332 | bot._client.removeAllListeners('D2GS_PLAYERMOVE') 333 | bot.follow = false 334 | bot.say(`Follow off`) 335 | } catch (error) { 336 | bot.say('Can\'t find any warp') 337 | } 338 | } 339 | 340 | bot.takeWaypoint = async (level) => { 341 | // Should we move this to a property of bot to avoid looping the array everytime we use the wp ? Or not 342 | // let waypoint = bot.objects.find(object => { return diablo2Data.objects[object['objectUniqueCode']]['description - not loaded'].includes('waypoint') }) 343 | let waypoint 344 | for (let i = bot.objects.length - 1; i > 0; i--) { // We start at the end, just in case, so we check the latest received objects 345 | if (diablo2Data.objects[bot.objects[i]['objectUniqueCode']]['description - not loaded'].includes('waypoint')) { 346 | waypoint = bot.objects[i] 347 | } 348 | } 349 | if (waypoint === undefined) { // No waypoint in my area !!! 350 | bot.say(`No waypoint in area ${bot.area}`) 351 | return false 352 | } 353 | await bot.moveTo(false, waypoint['x'], waypoint['y']) 354 | const area = diablo2Data.areasByName[level] 355 | bot._client.once('D2GS_WAYPOINTMENU', ({ unitId, availableWaypoints }) => { 356 | bot._client.write('D2GS_WAYPOINT', { // TODO: Handle the case where the bot aint got the wp 357 | waypointId: unitId, 358 | levelNumber: area === undefined ? level : area['id'] // Allows to use this function with the name of the level or the id 359 | }) 360 | }) 361 | await bot.moveToEntity(false, waypoint['objectId']) 362 | bot._client.write('D2GS_INTERACTWITHENTITY', { 363 | entityType: waypoint['objectType'], 364 | entityId: waypoint['objectId'] 365 | }) 366 | } 367 | 368 | bot.base = async () => { 369 | return new Promise(resolve => { 370 | // Callback that will be triggered when opening portal 371 | const callback = ({ ownerId, ownerName, localId, remoteId }) => { 372 | bot._client.write('D2GS_RUNTOENTITY', { // Go to the portal 373 | entityType: 2, 374 | entityId: localId 375 | }) 376 | bot._client.write('D2GS_INTERACTWITHENTITY', { // Interact with it 377 | entityType: 2, 378 | entityId: localId 379 | }) 380 | resolve() 381 | } 382 | bot._client.once('D2GS_PORTALOWNERSHIP', callback) 383 | if (bot.hasItem('tbk')) { // If we have a tome of portal 384 | bot.castSkillOnLocation(bot.x, bot.y, 220) 385 | } else { // TODO: should check if we got scrolls in tome or inventory btw 386 | bot._client.write('D2GS_USEITEM', { 387 | itemId: 73, // Hardcoded but it's tome of portal 388 | x: bot.x, 389 | y: bot.y 390 | }) 391 | } 392 | 393 | setTimeout(() => { 394 | if (bot._client.removeListener('D2GS_PORTALOWNERSHIP', callback)) { 395 | bot.say(`Failed to go to town`) // Maybe can happen that we didn't receive the D2GS_PORTALOWNERSHIP ? 396 | } 397 | resolve() 398 | }, 2000) 399 | bot.say(`Opened portal ${bot.scrolls.portal} portals left`) 400 | }) 401 | } 402 | } 403 | 404 | module.exports = inject 405 | --------------------------------------------------------------------------------