/
index.js
328 lines (288 loc) · 9.33 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
const { performance } = require('perf_hooks')
const AStar = require('./lib/astar')
const Move = require('./lib/move')
const Vec3 = require('vec3').Vec3
const { PlayerState } = require('prismarine-physics')
const nbt = require('prismarine-nbt')
const THINK_TIMEOUT = 40 // ms
function inject (bot) {
bot.pathfinder = {}
bot.pathfinder.bestHarvestTool = function (block) {
const availableTools = bot.inventory.items()
const effects = bot.entity.effects
let fastest = Number.MAX_VALUE
let bestTool = null
for (const tool of availableTools) {
const enchants = (tool && tool.nbt) ? nbt.simplify(tool.nbt).Enchantments : []
const digTime = block.digTime(tool ? tool.type : null, false, false, false, enchants, effects)
if (digTime < fastest) {
fastest = digTime
bestTool = tool
}
}
return bestTool
}
bot.pathfinder.getPathTo = function (movements, goal, done, timeout) {
const p = bot.entity.position
const start = new Move(p.x, p.y, p.z, movements.countScaffoldingItems(), 0)
done(new AStar(start, movements, goal, timeout || THINK_TIMEOUT).compute())
}
let stateMovements = null
let stateGoal = null
let dynamicGoal = false
let path = []
let pathUpdated = false
let digging = false
let placing = false
let placingBlock = null
let thinking = false
let lastNodeTime = performance.now()
function resetPath (clearStates = true) {
path = []
if (digging) bot.stopDigging()
digging = false
placing = false
pathUpdated = false
if (clearStates) { bot.clearControlStates() }
}
bot.pathfinder.setGoal = function (goal, dynamic = false) {
stateGoal = goal
dynamicGoal = dynamic
resetPath()
}
bot.pathfinder.setMovements = function (movements) {
stateMovements = movements
resetPath()
}
bot.pathfinder.isMoving = function () {
return path.length > 0 || thinking
}
bot.pathfinder.isMining = function () {
return digging
}
bot.pathfinder.isBuilding = function () {
return placing
}
bot.pathfinder.isThinking = function () {
return thinking
}
bot.on('physicTick', monitorMovement)
function isPositionNearPath (pos, path) {
for (const i in path) {
const node = path[i]
const dx = Math.abs(node.x - pos.x)
const dy = Math.abs(node.y - pos.y)
const dz = Math.abs(node.z - pos.z)
if (dx <= 1 && dy <= 2 && dz <= 1) return true
}
return false
}
// Return the average x/z position of the highest standing positions
// in the block.
function getPositionOnTopOf (block) {
if (block.shapes.length === 0) return null
const p = new Vec3(0.5, 0, 0.5)
let n = 1
for (const shape of block.shapes) {
const h = shape[4]
if (h === p.y) {
p.x += (shape[0] + shape[3]) / 2
p.z += (shape[2] + shape[5]) / 2
n++
} else if (h > p.y) {
n = 2
p.x = 0.5 + (shape[0] + shape[3]) / 2
p.y = h
p.z = 0.5 + (shape[2] + shape[5]) / 2
}
}
p.x /= n
p.z /= n
return block.position.plus(p)
}
function fullStop () {
bot.clearControlStates()
// Force horizontal velocity to 0 (otherwise inertia can move us too far)
// Kind of cheaty, but the server will not tell the difference
bot.entity.velocity.x = 0
bot.entity.velocity.z = 0
const blockX = Math.floor(bot.entity.position.x) + 0.5
const blockZ = Math.floor(bot.entity.position.z) + 0.5
// Make sure our bounding box don't collide with neighboring blocks
// otherwise recenter the position
if (Math.abs(bot.entity.position.x - blockX) > 0.2) { bot.entity.position.x = blockX }
if (Math.abs(bot.entity.position.z - blockZ) > 0.2) { bot.entity.position.z = blockZ }
}
bot.on('blockUpdate', (oldBlock, newBlock) => {
if (isPositionNearPath(oldBlock.position, path) && oldBlock.type !== newBlock.type) {
resetPath(false)
}
})
bot.on('chunkColumnLoad', (chunk) => {
resetPath()
})
function canStraightPathTo (pos) {
const state = new PlayerState(bot, {
forward: true,
back: false,
left: false,
right: false,
jump: false,
sprint: false,
sneak: false
})
const delta = pos.minus(bot.entity.position)
state.yaw = Math.atan2(-delta.x, -delta.z)
const world = { getBlock: (pos) => { return bot.blockAt(pos, false) } }
for (let step = 0; step < 1000; step++) {
bot.physics.simulatePlayer(state, world)
if (pos.distanceTo(state.pos) <= 2) return true
// TODO: check blocks to avoid
if (!state.onGround || state.isCollidedHorizontally) return false
}
return false
}
function monitorMovement () {
// Test freemotion
if (stateMovements && stateMovements.allowFreeMotion && stateGoal && stateGoal.entity) {
const target = stateGoal.entity
if (canStraightPathTo(target.position)) {
bot.lookAt(target.position.offset(0, 1.6, 0))
if (target.position.distanceTo(bot.entity.position) > Math.sqrt(stateGoal.rangeSq)) {
bot.setControlState('forward', true)
} else {
bot.clearControlStates()
}
return
}
}
if (stateGoal && stateGoal.hasChanged()) {
resetPath()
}
if (path.length === 0) {
lastNodeTime = performance.now()
if (stateGoal && stateMovements && !thinking) {
if (stateGoal.isEnd(bot.entity.position.floored()) || pathUpdated) {
if (!dynamicGoal) {
bot.emit('goal_reached', stateGoal)
stateGoal = null
}
} else {
thinking = true
bot.pathfinder.getPathTo(stateMovements, stateGoal, (results) => {
bot.emit('path_update', results)
path = results.path
thinking = false
pathUpdated = true
})
}
}
return
}
let nextPoint = path[0]
let np = getPositionOnTopOf(bot.blockAt(new Vec3(nextPoint.x, nextPoint.y, nextPoint.z)))
if (np === null) np = getPositionOnTopOf(bot.blockAt(new Vec3(nextPoint.x, nextPoint.y - 1, nextPoint.z)))
nextPoint.x = np.x
nextPoint.y = np.y
nextPoint.z = np.z
const p = bot.entity.position
// Handle digging
if (digging || nextPoint.toBreak.length > 0) {
if (!digging && bot.entity.onGround) {
digging = true
const b = nextPoint.toBreak.shift()
const block = bot.blockAt(new Vec3(b.x, b.y, b.z), false)
const tool = bot.pathfinder.bestHarvestTool(block)
fullStop()
bot.equip(tool, 'hand', function () {
bot.dig(block, function (err) {
lastNodeTime = performance.now()
if (err) resetPath()
digging = false
})
})
}
return
}
// Handle block placement
// TODO: sneak when placing or make sure the block is not interactive
if (placing || nextPoint.toPlace.length > 0) {
if (!placing) {
placing = true
placingBlock = nextPoint.toPlace.shift()
fullStop()
}
const block = stateMovements.getScaffoldingItem()
if (!block) {
resetPath()
return
}
let canPlace = true
if (placingBlock.jump) {
bot.setControlState('jump', true)
canPlace = placingBlock.y + 1 < bot.entity.position.y
}
if (canPlace) {
bot.equip(block, 'hand', function () {
const refBlock = bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), false)
bot.placeBlock(refBlock, new Vec3(placingBlock.dx, placingBlock.dy, placingBlock.dz), function (err) {
placing = false
lastNodeTime = performance.now()
if (err) resetPath()
})
})
}
return
}
const dx = nextPoint.x - p.x
const dy = nextPoint.y - p.y
const dz = nextPoint.z - p.z
if ((dx * dx + dz * dz) <= 0.15 * 0.15 && (bot.entity.onGround || bot.entity.isInWater)) {
// arrived at next point
lastNodeTime = performance.now()
path.shift()
if (path.length === 0) { // done
if (!dynamicGoal && stateGoal.isEnd(p.floored())) {
bot.emit('goal_reached', stateGoal)
stateGoal = null
}
fullStop()
return
}
// not done yet
nextPoint = path[0]
if (nextPoint.toBreak.length > 0 || nextPoint.toPlace.length > 0) {
fullStop()
}
return
}
let gottaJump = false
const horizontalDelta = Math.sqrt(dx * dx + dz * dz)
if (dy > 0.6) {
// gotta jump up when we're close enough
gottaJump = horizontalDelta < 1.75
} else if (dy > -0.1 && nextPoint.parkour) {
// possibly jump over a hole
gottaJump = horizontalDelta > 1.5 && horizontalDelta < 2.5
}
gottaJump = gottaJump || bot.entity.isInWater
bot.setControlState('jump', gottaJump)
// run toward next point
bot.look(Math.atan2(-dx, -dz), 0)
const lx = -Math.sin(bot.entity.yaw)
const lz = -Math.cos(bot.entity.yaw)
const frontBackProj = lx * dx + lz * dz
bot.setControlState('forward', frontBackProj > 0)
bot.setControlState('back', frontBackProj < 0)
// check for futility
if (performance.now() - lastNodeTime > 1500) {
// should never take this long to go to the next node
resetPath()
}
}
}
module.exports = {
pathfinder: inject,
Movements: require('./lib/movements'),
goals: require('./lib/goals')
}