├── .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 | [](http://npmjs.com/package/autotathamet)
3 | [](https://circleci.com/gh/MephisTools/AutoTathamet)
4 | [](https://discord.gg/9RqtApv)
5 | [](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 |
--------------------------------------------------------------------------------