spectra-physics 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/index.js +1 -0
- package/lib/client/index.js +4 -0
- package/lib/client/simulation.js +35 -0
- package/lib/engine/player/index.js +11 -0
- package/lib/pathfinder/astar.js +167 -0
- package/lib/pathfinder/costs.js +21 -0
- package/lib/pathfinder/digger.js +132 -0
- package/lib/pathfinder/goals.js +236 -0
- package/lib/pathfinder/index.js +434 -0
- package/lib/pathfinder/moves.js +223 -0
- package/lib/pathfinder/terrain.js +169 -0
- package/package.json +4 -3
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Pathfinder that walks with the engine itself: A* over moves costed in ticks, and a follower that picks
|
|
3
|
+
// keys by simulating the player a few ticks ahead, turns like a mouse and breaks blocks tick by tick.
|
|
4
|
+
// The API mirrors mineflayer-pathfinder (setGoal, goto, stop, goals, path events).
|
|
5
|
+
|
|
6
|
+
const { Vec3 } = require('vec3')
|
|
7
|
+
const { AStar } = require('./astar')
|
|
8
|
+
const { Terrain } = require('./terrain')
|
|
9
|
+
const { installDigger, bestTool } = require('./digger')
|
|
10
|
+
const { f32, wrapDegrees } = require('../engine/mth')
|
|
11
|
+
|
|
12
|
+
const MOUSE_STEP = f32(0.15)
|
|
13
|
+
const STUCK_TICKS = 40
|
|
14
|
+
const LOOKAHEAD = 4
|
|
15
|
+
const KEYS = ['forward', 'back', 'left', 'right', 'jump', 'sprint', 'sneak']
|
|
16
|
+
|
|
17
|
+
const conv = require('mineflayer/lib/conversions')
|
|
18
|
+
const toDegrees = (yaw) => conv.toNotchianYaw(yaw)
|
|
19
|
+
|
|
20
|
+
function installPathfinder (ctx) {
|
|
21
|
+
const { bot, player, world, state } = ctx
|
|
22
|
+
installDigger(ctx)
|
|
23
|
+
|
|
24
|
+
const settings = {
|
|
25
|
+
canDig: true,
|
|
26
|
+
allowParkour: true,
|
|
27
|
+
allowLongParkour: false,
|
|
28
|
+
allowSprinting: true,
|
|
29
|
+
maxDropDown: 3,
|
|
30
|
+
maxDigTicks: 400,
|
|
31
|
+
thinkTimeout: 5000,
|
|
32
|
+
tickTimeout: 25,
|
|
33
|
+
maxNodes: 200000
|
|
34
|
+
}
|
|
35
|
+
const terrain = new Terrain(world, settings)
|
|
36
|
+
|
|
37
|
+
let goal = null
|
|
38
|
+
let dynamic = false
|
|
39
|
+
let search = null
|
|
40
|
+
let path = []
|
|
41
|
+
let index = 0
|
|
42
|
+
let needPlan = false
|
|
43
|
+
let digging = false
|
|
44
|
+
let stuck = { ticks: 0, pos: null }
|
|
45
|
+
let owned = false
|
|
46
|
+
|
|
47
|
+
// ---- keys and looking -------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function keys (k = {}) {
|
|
50
|
+
if (process.env.SPECTRA_PATH_DEBUG) {
|
|
51
|
+
const n = path[index]
|
|
52
|
+
const p = player.pos
|
|
53
|
+
console.log(`[path] ${index}/${path.length} ${n ? n.kind + '@' + n.x + ',' + n.y + ',' + n.z : '-'} pos ${p.x.toFixed(2)},${p.y.toFixed(2)},${p.z.toFixed(2)} yaw ${player.yRot.toFixed(1)} keys ${KEYS.filter(key => k[key]).join('+')}`)
|
|
54
|
+
}
|
|
55
|
+
owned = true
|
|
56
|
+
for (const key of KEYS) bot.setControlState(key, !!k[key])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function release () {
|
|
60
|
+
if (!owned) return
|
|
61
|
+
owned = false
|
|
62
|
+
for (const key of KEYS) bot.setControlState(key, false)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// where the mouse will be after a tick of turning toward yaw (game degrees), as controls.turn does it
|
|
66
|
+
function turnStep (from, to) {
|
|
67
|
+
const max = (ctx.options.turnSpeed ?? 35) / MOUSE_STEP
|
|
68
|
+
const steps = Math.max(-max, Math.min(max, Math.round(wrapDegrees(to - from) / MOUSE_STEP)))
|
|
69
|
+
return steps === 0 ? from : f32(from + f32(f32(steps) * MOUSE_STEP))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function yawTo (x, z) {
|
|
73
|
+
return Math.atan2(-(x - player.pos.x), -(z - player.pos.z))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- looking ahead with the engine -------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function simulate (k, yaw, ticks, until) {
|
|
79
|
+
const sim = player.clone()
|
|
80
|
+
const target = toDegrees(yaw)
|
|
81
|
+
const input = { forward: false, back: false, left: false, right: false, jump: false, sprint: false, sneak: false, ...k }
|
|
82
|
+
for (let i = 0; i < ticks; i++) {
|
|
83
|
+
sim.yRot = turnStep(sim.yRot, target)
|
|
84
|
+
sim.tick(input)
|
|
85
|
+
const r = until(sim, i)
|
|
86
|
+
if (r !== undefined) return r ? sim : null
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reachedBy (sim, n) {
|
|
92
|
+
const dx = sim.pos.x - (n.x + 0.5)
|
|
93
|
+
const dz = sim.pos.z - (n.z + 0.5)
|
|
94
|
+
if (Math.abs(dx) > 0.35 || Math.abs(dz) > 0.35) return false
|
|
95
|
+
if (n.water) return sim.pos.y >= n.y - 0.3 && sim.pos.y <= n.y + 1.2
|
|
96
|
+
if (n.climb) return sim.pos.y >= n.floor - 0.2 && sim.pos.y < n.floor + 1
|
|
97
|
+
return Math.abs(sim.pos.y - n.floor) < 0.1 && sim.onGround
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reached (n) { return reachedBy(player, n) }
|
|
101
|
+
|
|
102
|
+
// the run toward n with these keys gets there without falling below it or stalling
|
|
103
|
+
function canRun (k, n, ticks = 30) {
|
|
104
|
+
const yaw = yawTo(n.x + 0.5, n.z + 0.5)
|
|
105
|
+
return !!simulate(k, yaw, ticks, (sim) => {
|
|
106
|
+
if (reachedBy(sim, n)) return true
|
|
107
|
+
if (!n.water && sim.pos.y < Math.min(n.floor, player.pos.y) - 0.6) return false
|
|
108
|
+
if (sim.horizontalCollision && sim.onGround && !k.jump) return false
|
|
109
|
+
return undefined
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- planning ----------------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
function currentNode () {
|
|
116
|
+
const p = player.pos
|
|
117
|
+
const x = Math.floor(p.x)
|
|
118
|
+
const z = Math.floor(p.z)
|
|
119
|
+
if (player.isInWater()) {
|
|
120
|
+
const y = Math.floor(p.y + 0.1)
|
|
121
|
+
return { x, y, z, floor: y, water: true, climb: false, dig: [] }
|
|
122
|
+
}
|
|
123
|
+
const feetY = Math.floor(p.y + 0.2)
|
|
124
|
+
const feet = terrain.at(x, Math.floor(p.y), z)
|
|
125
|
+
if (feet?.climbable && !player.onGround) return { x, y: Math.floor(p.y), z, floor: Math.floor(p.y), water: false, climb: true, dig: [] }
|
|
126
|
+
if (player.onGround) return { x, y: feetY, z, floor: p.y, water: false, climb: !!feet?.climbable, dig: [] }
|
|
127
|
+
// in the air: plan from the floor right below, unless still falling far
|
|
128
|
+
for (let y = feetY; y >= feetY - 2; y--) {
|
|
129
|
+
const floor = terrain.floorAt(x, y, z)
|
|
130
|
+
if (floor != null && p.y - floor < 1.5) return { x, y, z, floor, water: false, climb: false, dig: [] }
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function plan () {
|
|
136
|
+
const start = currentNode()
|
|
137
|
+
if (!start) return false
|
|
138
|
+
needPlan = false
|
|
139
|
+
terrain.prepare(bot)
|
|
140
|
+
terrain.traits.length = 0
|
|
141
|
+
search = new AStar(start, goal, terrain, settings)
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function adopt (result) {
|
|
146
|
+
path = result.path
|
|
147
|
+
index = 0
|
|
148
|
+
aimed = -1
|
|
149
|
+
stuck = { ticks: 0, pos: null }
|
|
150
|
+
bot.emit('path_update', result)
|
|
151
|
+
if (result.status === 'noPath' && !path.length) bot.emit('path_reset', 'no_path')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function reset (reason) {
|
|
155
|
+
path = []
|
|
156
|
+
index = 0
|
|
157
|
+
search = null
|
|
158
|
+
needPlan = true
|
|
159
|
+
bot.emit('path_reset', reason)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- following ---------------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
const flat = (n) => n.kind === 'walk' && !n.dig.length && !n.water && !n.climb
|
|
165
|
+
const dist2 = (ax, az, bx, bz) => (ax - bx) ** 2 + (az - bz) ** 2
|
|
166
|
+
|
|
167
|
+
function advance () {
|
|
168
|
+
for (let i = Math.min(path.length - 1, index + LOOKAHEAD); i >= index; i--) {
|
|
169
|
+
if (reached(path[i]) && !path.slice(index, i + 1).some(n => pendingDig(n).length)) {
|
|
170
|
+
index = i + 1
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// cutting corners on level ground: the nearest of the next nodes is where we are on the route
|
|
175
|
+
const p = player.pos
|
|
176
|
+
if (player.onGround && index < path.length && flat(path[index])) {
|
|
177
|
+
let best = index
|
|
178
|
+
let bestD = Infinity
|
|
179
|
+
for (let i = index; i < path.length && i <= index + LOOKAHEAD; i++) {
|
|
180
|
+
const n = path[i]
|
|
181
|
+
if (!flat(n) || Math.abs(p.y - n.floor) > 0.6) break
|
|
182
|
+
const d = dist2(p.x, p.z, n.x + 0.5, n.z + 0.5)
|
|
183
|
+
if (d < bestD) { bestD = d; best = i }
|
|
184
|
+
}
|
|
185
|
+
index = best
|
|
186
|
+
}
|
|
187
|
+
// a node is behind us once the next one is closer than it is
|
|
188
|
+
while (index + 1 < path.length && player.onGround) {
|
|
189
|
+
const a = path[index]
|
|
190
|
+
const b = path[index + 1]
|
|
191
|
+
if (!flat(a) || !flat(b) || Math.abs(a.floor - b.floor) > 0.6 || Math.abs(p.y - a.floor) > 0.6) break
|
|
192
|
+
if (dist2(p.x, p.z, b.x + 0.5, b.z + 0.5) >= dist2(a.x + 0.5, a.z + 0.5, b.x + 0.5, b.z + 0.5)) break
|
|
193
|
+
index++
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function pendingDig (n) {
|
|
198
|
+
return n.dig.filter(([x, y, z]) => {
|
|
199
|
+
const b = bot.blockAt(new Vec3(x, y, z))
|
|
200
|
+
return b && b.boundingBox !== 'empty' && b.name !== 'air'
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// the furthest walk node straight ahead that a sprint reaches without detours; kept while it still works
|
|
205
|
+
let aimed = -1
|
|
206
|
+
function aimIndex (sprint) {
|
|
207
|
+
if (aimed > index && aimed < path.length && aimed <= index + LOOKAHEAD && canRun({ forward: true, sprint }, path[aimed], 40)) return aimed
|
|
208
|
+
aimed = pickAim(sprint)
|
|
209
|
+
return aimed
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function pickAim (sprint) {
|
|
213
|
+
let target = index
|
|
214
|
+
const first = path[index]
|
|
215
|
+
if (first.kind !== 'walk' || first.dig.length || first.water || first.climb) return target
|
|
216
|
+
for (let i = index + 1; i < path.length && i <= index + LOOKAHEAD; i++) {
|
|
217
|
+
const n = path[i]
|
|
218
|
+
if (n.kind !== 'walk' || n.dig.length || n.water || n.climb || Math.abs(n.floor - first.floor) > 0.6) break
|
|
219
|
+
if (!canRun({ forward: true, sprint }, n, 40)) break
|
|
220
|
+
target = i
|
|
221
|
+
}
|
|
222
|
+
return target
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function moveToward (n, i) {
|
|
226
|
+
const yaw = yawTo(n.x + 0.5, n.z + 0.5)
|
|
227
|
+
// only the yaw follows the route; the pitch stays where the player left it
|
|
228
|
+
bot.look(yaw, bot.entity.pitch).catch(() => {})
|
|
229
|
+
const off = Math.abs(wrapDegrees(toDegrees(yaw) - player.yRot))
|
|
230
|
+
const facing = off < 90
|
|
231
|
+
const sprintOk = settings.allowSprinting && off < 30 && !player.isInWater()
|
|
232
|
+
const dxz = Math.hypot(n.x + 0.5 - player.pos.x, n.z + 0.5 - player.pos.z)
|
|
233
|
+
|
|
234
|
+
if (n.water || player.isInWater()) {
|
|
235
|
+
const rise = n.y >= Math.floor(player.pos.y) || n.kind === 'jump'
|
|
236
|
+
// holding sneak sinks four times faster than drifting down
|
|
237
|
+
const sink = n.water && n.y < Math.floor(player.pos.y)
|
|
238
|
+
return keys({ forward: dxz > 0.2, jump: rise && !sink, sneak: sink })
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (n.climb && n.kind === 'climb') {
|
|
242
|
+
if (n.floor > player.pos.y + 0.1) return keys({ jump: true, forward: dxz > 0.25 })
|
|
243
|
+
return keys({ forward: dxz > 0.25 })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (n.kind === 'climb' || (n.kind === 'jump' && path[i - 1]?.climb)) {
|
|
247
|
+
return keys({ forward: true, jump: true })
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (n.kind === 'jump' || n.kind === 'parkour') {
|
|
251
|
+
const sprint = sprintOk && (n.kind === 'parkour' || dxz > 1.5)
|
|
252
|
+
const jumpNow = player.onGround && canRun({ forward: true, sprint, jump: true }, n, 25)
|
|
253
|
+
if (jumpNow) return keys({ forward: true, sprint, jump: true })
|
|
254
|
+
// about to run out of floor before the jump would work: jump anyway
|
|
255
|
+
const next = simulate({ forward: true, sprint }, yaw, 1, (sim) => sim.onGround)
|
|
256
|
+
if (n.kind === 'parkour' && !next && player.onGround) return keys({ forward: true, sprint, jump: true })
|
|
257
|
+
const blocked = player.horizontalCollision && player.onGround
|
|
258
|
+
return keys({ forward: facing || dxz < 0.5, sprint, jump: blocked })
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (n.kind === 'dig-down') return keys({ forward: dxz > 0.3 && facing })
|
|
262
|
+
|
|
263
|
+
// walk / drop
|
|
264
|
+
const sprint = sprintOk && dxz > 0.8 && canRun({ forward: true, sprint: true }, n, 30)
|
|
265
|
+
return keys({ forward: facing || dxz < 0.3, sprint })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function follow () {
|
|
269
|
+
advance()
|
|
270
|
+
if (index >= path.length) {
|
|
271
|
+
path = []
|
|
272
|
+
const here = currentNode()
|
|
273
|
+
if (here && goal.isEnd(here)) return arrive()
|
|
274
|
+
needPlan = true
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const n = path[index]
|
|
279
|
+
const dig = pendingDig(n)
|
|
280
|
+
if (dig.length) {
|
|
281
|
+
if (Math.hypot(player.vel.x, player.vel.z) > 0.03 || (!player.onGround && !n.climb && !player.isInWater())) return release()
|
|
282
|
+
release()
|
|
283
|
+
const [x, y, z] = dig[0]
|
|
284
|
+
const block = bot.blockAt(new Vec3(x, y, z))
|
|
285
|
+
digging = true
|
|
286
|
+
ctx.dig(block).catch(() => reset('dig_failed')).then(() => { digging = false })
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const aim = n.kind === 'walk' ? aimIndex(settings.allowSprinting) : index
|
|
291
|
+
moveToward(path[aim], aim)
|
|
292
|
+
|
|
293
|
+
// not moving for a while, or pushed far off the route: plan again from here
|
|
294
|
+
const p = player.pos
|
|
295
|
+
if (!stuck.pos || Math.hypot(p.x - stuck.pos.x, p.y - stuck.pos.y, p.z - stuck.pos.z) > 0.15) {
|
|
296
|
+
stuck = { ticks: 0, pos: { x: p.x, y: p.y, z: p.z } }
|
|
297
|
+
} else if (++stuck.ticks > STUCK_TICKS) return reset('stuck')
|
|
298
|
+
const target = path[index]
|
|
299
|
+
if (Math.hypot(target.x + 0.5 - p.x, target.floor - p.y, target.z + 0.5 - p.z) > 4) reset('off_path')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function arrive () {
|
|
303
|
+
release()
|
|
304
|
+
if (dynamic) return
|
|
305
|
+
const reachedGoal = goal
|
|
306
|
+
goal = null
|
|
307
|
+
bot.emit('goal_reached', reachedGoal)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function tick () {
|
|
311
|
+
if (!goal || !bot.entity) return
|
|
312
|
+
if (!goal.isValid()) return pf.stop()
|
|
313
|
+
if (goal.hasChanged()) needPlan = true
|
|
314
|
+
if (digging || ctx.isDigging()) return
|
|
315
|
+
|
|
316
|
+
if (needPlan && !search) {
|
|
317
|
+
if (!path.length) release()
|
|
318
|
+
plan()
|
|
319
|
+
}
|
|
320
|
+
if (search) {
|
|
321
|
+
const result = search.compute()
|
|
322
|
+
if (result.status === 'partial') {
|
|
323
|
+
if (!path.length) release()
|
|
324
|
+
else follow()
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
search = null
|
|
328
|
+
adopt(result)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!path.length) {
|
|
332
|
+
const here = currentNode()
|
|
333
|
+
if (here && goal.isEnd(here)) return arrive()
|
|
334
|
+
if (!needPlan) needPlan = true
|
|
335
|
+
return release()
|
|
336
|
+
}
|
|
337
|
+
follow()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
bot.on('physicsTick', () => {
|
|
341
|
+
try { tick() } catch (err) { reset('error'); bot.emit('error', err) }
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// blocks changing on the planned route make it stale
|
|
345
|
+
bot.on('blockUpdate', (oldBlock, newBlock) => {
|
|
346
|
+
if (!path.length || !newBlock || oldBlock?.type === newBlock.type) return
|
|
347
|
+
const p = newBlock.position
|
|
348
|
+
// blocks we are digging ourselves are expected to change
|
|
349
|
+
for (const n of path) if (n.dig.some(([x, y, z]) => x === p.x && y === p.y && z === p.z)) return
|
|
350
|
+
for (let i = index; i < path.length && i < index + 20; i++) {
|
|
351
|
+
const n = path[i]
|
|
352
|
+
if (p.x === n.x && p.z === n.z && p.y >= Math.floor(n.floor) - 1 && p.y <= Math.floor(n.floor) + 2) return reset('block_updated')
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
bot.on('death', () => pf.stop())
|
|
357
|
+
|
|
358
|
+
const pf = {
|
|
359
|
+
settings,
|
|
360
|
+
goals: require('./goals'),
|
|
361
|
+
|
|
362
|
+
setGoal (newGoal, isDynamic = false) {
|
|
363
|
+
goal = newGoal
|
|
364
|
+
dynamic = isDynamic
|
|
365
|
+
path = []
|
|
366
|
+
index = 0
|
|
367
|
+
search = null
|
|
368
|
+
needPlan = !!newGoal
|
|
369
|
+
if (!newGoal) {
|
|
370
|
+
if (ctx.isDigging()) ctx.stopDigging()
|
|
371
|
+
release()
|
|
372
|
+
}
|
|
373
|
+
bot.emit('goal_updated', newGoal, isDynamic)
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
goto (newGoal) {
|
|
377
|
+
return new Promise((resolve, reject) => {
|
|
378
|
+
const cleanup = () => {
|
|
379
|
+
bot.removeListener('goal_reached', onReached)
|
|
380
|
+
bot.removeListener('path_update', onUpdate)
|
|
381
|
+
bot.removeListener('path_stop', onStop)
|
|
382
|
+
bot.removeListener('end', onEnd)
|
|
383
|
+
}
|
|
384
|
+
const onReached = (g) => { if (g === newGoal) { cleanup(); resolve() } }
|
|
385
|
+
const onUpdate = (r) => {
|
|
386
|
+
if (r.status === 'noPath' && r.path.length === 0) { cleanup(); pf.setGoal(null); reject(new Error('No path to the goal')) }
|
|
387
|
+
if (r.status === 'timeout' && r.path.length === 0) { cleanup(); pf.setGoal(null); reject(new Error('Took to long to decide path to goal')) }
|
|
388
|
+
}
|
|
389
|
+
const onStop = () => { cleanup(); reject(new Error('Path was stopped before it could be completed')) }
|
|
390
|
+
const onEnd = () => { cleanup(); reject(new Error('Bot disconnected')) }
|
|
391
|
+
bot.on('goal_reached', onReached)
|
|
392
|
+
bot.on('path_update', onUpdate)
|
|
393
|
+
bot.on('path_stop', onStop)
|
|
394
|
+
bot.on('end', onEnd)
|
|
395
|
+
pf.setGoal(newGoal, false)
|
|
396
|
+
})
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
stop () {
|
|
400
|
+
if (!goal) return
|
|
401
|
+
pf.setGoal(null)
|
|
402
|
+
bot.emit('path_stop')
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
// a full search right now, without moving (for checks and tests)
|
|
406
|
+
getPathTo (target, startNode = currentNode()) {
|
|
407
|
+
if (!startNode) return { status: 'noPath', path: [] }
|
|
408
|
+
terrain.prepare(bot)
|
|
409
|
+
const s = new AStar(startNode, target, terrain, { ...settings, tickTimeout: settings.thinkTimeout })
|
|
410
|
+
return s.compute()
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
setMovements (movements) {
|
|
414
|
+
for (const key of ['canDig', 'allowParkour', 'allowSprinting', 'maxDropDown']) {
|
|
415
|
+
if (movements && key in movements) settings[key] = movements[key]
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
bestHarvestTool: (block) => bestTool(bot, block),
|
|
420
|
+
isMoving: () => path.length > 0 && index < path.length,
|
|
421
|
+
isMining: () => digging || ctx.isDigging(),
|
|
422
|
+
isBuilding: () => false,
|
|
423
|
+
isThinking: () => !!search,
|
|
424
|
+
get goal () { return goal },
|
|
425
|
+
get path () { return path.slice(index) }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
bot.spectra.pathfinder = pf
|
|
429
|
+
bot.spectra.dig = (block) => ctx.dig(block)
|
|
430
|
+
if (!bot.pathfinder) bot.pathfinder = pf
|
|
431
|
+
state.pathfinder = pf
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = { installPathfinder, goals: require('./goals') }
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Neighbours of a node for A*. A node is the block the feet are in plus the exact floor height, so slabs,
|
|
3
|
+
// stairs and carpets are walked over rather than jumped. Costs are in ticks (see costs.js).
|
|
4
|
+
|
|
5
|
+
const { COST } = require('./costs')
|
|
6
|
+
const { PLAYER_HEIGHT, STEP } = require('./terrain')
|
|
7
|
+
|
|
8
|
+
const JUMP = 1.25 // highest floor a jump reaches
|
|
9
|
+
const CARDINAL = [[1, 0], [-1, 0], [0, 1], [0, -1]]
|
|
10
|
+
const DIAGONAL = [[1, 1], [1, -1], [-1, 1], [-1, -1]]
|
|
11
|
+
|
|
12
|
+
function node (x, y, z, floor, kind, cost, extra) {
|
|
13
|
+
return { x, y, z, floor, kind, cost, dig: extra?.dig ?? [], water: !!extra?.water, climb: !!extra?.climb, gap: extra?.gap ?? 0 }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function digCost (terrain, dig) {
|
|
17
|
+
let ticks = 0
|
|
18
|
+
for (const d of dig) ticks += terrain.digCost(d[3]) + COST.digSetup
|
|
19
|
+
return ticks
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// blocks between two heights in a column are free for the body (nothing dug)
|
|
23
|
+
function columnClear (terrain, x, z, from, to) {
|
|
24
|
+
for (let y = Math.floor(from); y <= Math.floor(to - 1e-6); y++) {
|
|
25
|
+
const t = terrain.at(x, y, z)
|
|
26
|
+
if (!t || t.danger || !t.empty) return false
|
|
27
|
+
}
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// headroom above the current position for a jump of `rise`
|
|
32
|
+
function jumpRoom (terrain, n, rise, dig) {
|
|
33
|
+
const top = n.floor + PLAYER_HEIGHT + rise
|
|
34
|
+
for (let y = Math.floor(n.floor + PLAYER_HEIGHT); y <= Math.floor(top - 1e-6); y++) {
|
|
35
|
+
const t = terrain.at(n.x, y, n.z)
|
|
36
|
+
if (!t || t.danger) return false
|
|
37
|
+
if (t.empty) continue
|
|
38
|
+
if (!dig || !terrain.canDigAt(n.x, y, n.z, t)) return false
|
|
39
|
+
dig.push([n.x, y, n.z, t])
|
|
40
|
+
}
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function waterNode (terrain, x, y, z) {
|
|
45
|
+
const feet = terrain.at(x, y, z)
|
|
46
|
+
if (!feet || !feet.water) return false
|
|
47
|
+
const head = terrain.at(x, y + 1, z)
|
|
48
|
+
return !!head && (head.water || head.empty) && !head.danger
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function climbNode (terrain, x, y, z) {
|
|
52
|
+
const feet = terrain.at(x, y, z)
|
|
53
|
+
if (!feet || !feet.climbable) return false
|
|
54
|
+
const head = terrain.at(x, y + 1, z)
|
|
55
|
+
return !!head && (head.empty || head.climbable) && !head.danger
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// where a player falling down column x/z from `fromFloor` ends up: a floor or water
|
|
59
|
+
function landing (terrain, x, startY, z, fromFloor, maxDrop) {
|
|
60
|
+
for (let y = startY; y >= startY - 64; y--) {
|
|
61
|
+
const t = terrain.at(x, y, z)
|
|
62
|
+
if (!t || t.danger) return null
|
|
63
|
+
if (t.water) return { y, floor: y, water: true }
|
|
64
|
+
const floor = terrain.floorAt(x, y, z)
|
|
65
|
+
if (floor != null) {
|
|
66
|
+
if (fromFloor - floor > maxDrop + 1e-6) return null
|
|
67
|
+
return { y, floor }
|
|
68
|
+
}
|
|
69
|
+
if (!t.empty && !t.climbable) return null
|
|
70
|
+
if (t.climbable) return { y, floor: y, climb: true }
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function neighbours (n, terrain, opts, out = []) {
|
|
76
|
+
const inWater = n.water
|
|
77
|
+
for (const [dx, dz] of CARDINAL) step(n, dx, dz, terrain, opts, out, false)
|
|
78
|
+
for (const [dx, dz] of DIAGONAL) step(n, dx, dz, terrain, opts, out, true)
|
|
79
|
+
if (opts.allowParkour && !inWater && !n.climb) for (const [dx, dz] of CARDINAL) parkour(n, dx, dz, terrain, opts, out)
|
|
80
|
+
vertical(n, terrain, opts, out)
|
|
81
|
+
return out
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// the column between two heights is free for the body; blocks in the way go to `dig` when allowed
|
|
85
|
+
function clearColumn (terrain, x, z, from, to, dig) {
|
|
86
|
+
const y0 = Math.floor(from)
|
|
87
|
+
for (let y = y0; y <= Math.floor(to - 1e-6); y++) {
|
|
88
|
+
const t = terrain.at(x, y, z)
|
|
89
|
+
if (!t || t.danger) return false
|
|
90
|
+
if (t.empty) continue
|
|
91
|
+
if (y === y0 && y + t.top <= from + 1e-6) continue
|
|
92
|
+
if (!dig || !terrain.canDigAt(x, y, z, t)) return false
|
|
93
|
+
if (!dig.some(d => d[0] === x && d[1] === y && d[2] === z)) dig.push([x, y, z, t])
|
|
94
|
+
}
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function step (n, dx, dz, terrain, opts, out, diagonal) {
|
|
99
|
+
const x = n.x + dx
|
|
100
|
+
const z = n.z + dz
|
|
101
|
+
const base = diagonal ? COST.diagonal : COST.walk
|
|
102
|
+
|
|
103
|
+
if (diagonal) {
|
|
104
|
+
// the box clips the corner unless both sides are open
|
|
105
|
+
const lo = n.floor
|
|
106
|
+
if (!columnClear(terrain, n.x + dx, n.z, lo, lo + PLAYER_HEIGHT)) return
|
|
107
|
+
if (!columnClear(terrain, n.x, n.z + dz, lo, lo + PLAYER_HEIGHT)) return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// into water
|
|
111
|
+
for (const y of [n.y, n.y + 1]) {
|
|
112
|
+
if (!waterNode(terrain, x, y, z)) continue
|
|
113
|
+
if (y > n.y && !jumpRoom(terrain, n, 1, null)) continue
|
|
114
|
+
if (!terrain.bodyFits(x, y, z, null)) continue
|
|
115
|
+
out.push(node(x, y, z, y, 'swim', COST.swim * (diagonal ? Math.SQRT2 : 1), { water: true }))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// up a block, level, or down a block; with digging the floor may be the block under a wall
|
|
120
|
+
const canDig = opts.canDig && !diagonal && !n.water
|
|
121
|
+
for (const y of [n.y + 1, n.y, n.y - 1]) {
|
|
122
|
+
let floor = terrain.floorAt(x, y, z)
|
|
123
|
+
if (floor == null && canDig) floor = terrain.floorUnder(x, y, z)
|
|
124
|
+
else if (y === n.y - 1) continue // a natural lower floor is a drop, handled below
|
|
125
|
+
if (floor == null) continue
|
|
126
|
+
const rise = floor - n.floor
|
|
127
|
+
if (rise > JUMP + 1e-6 || rise < -1.5) continue
|
|
128
|
+
if (rise < -STEP - 1e-6 && y !== n.y - 1) continue
|
|
129
|
+
const jump = rise > STEP + 1e-6
|
|
130
|
+
if (jump && diagonal) continue
|
|
131
|
+
if (jump && n.water && rise > 1 + 1e-6) continue
|
|
132
|
+
const dig = canDig ? [] : null
|
|
133
|
+
if (jump && !n.climb && !jumpRoom(terrain, n, rise, dig)) continue
|
|
134
|
+
if (!clearColumn(terrain, x, z, floor, Math.max(floor, n.floor) + PLAYER_HEIGHT, dig)) continue
|
|
135
|
+
let cost = jump ? COST.jumpUp : (rise < -STEP ? COST.dropDown : base)
|
|
136
|
+
if (n.water) cost += COST.swim
|
|
137
|
+
cost += terrain.floorPenalty(x, floor, z)
|
|
138
|
+
if (dig) cost += digCost(terrain, dig)
|
|
139
|
+
const kind = jump ? (n.climb ? 'climb' : 'jump') : (rise < -STEP ? 'drop' : 'walk')
|
|
140
|
+
out.push(node(x, y, z, floor, kind, cost, { dig: (dig ?? []).sort((a, b) => b[1] - a[1]) }))
|
|
141
|
+
if (!dig || dig.length === 0) return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// a climbable column next to us
|
|
145
|
+
if (climbNode(terrain, x, n.y, z) && terrain.bodyFits(x, n.y, z, null)) {
|
|
146
|
+
out.push(node(x, n.y, z, n.y, 'walk', base + COST.climbDown, { climb: true }))
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// walking off an edge
|
|
151
|
+
if (n.water) return
|
|
152
|
+
if (!columnClear(terrain, x, z, n.floor, n.floor + PLAYER_HEIGHT)) return
|
|
153
|
+
const land = landing(terrain, x, n.y - 1, z, n.floor, opts.maxDropDown)
|
|
154
|
+
if (!land) return
|
|
155
|
+
if (!land.water && !terrain.bodyFits(x, land.floor, z, null)) return
|
|
156
|
+
const fall = n.floor - land.floor
|
|
157
|
+
if (fall <= STEP + 1e-6) return
|
|
158
|
+
out.push(node(x, land.y, z, land.floor, 'drop', base + COST.fallTick * fall + 1, { water: land.water, climb: land.climb }))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// sprint-jump over a gap of one or two blocks (three with allowLongParkour)
|
|
162
|
+
function parkour (n, dx, dz, terrain, opts, out) {
|
|
163
|
+
const maxGap = opts.allowLongParkour ? 3 : 2
|
|
164
|
+
const top = n.floor + PLAYER_HEIGHT + 1.25
|
|
165
|
+
for (let g = 1; g <= maxGap; g++) {
|
|
166
|
+
const gx = n.x + dx * g
|
|
167
|
+
const gz = n.z + dz * g
|
|
168
|
+
// the gap column must be open from below the feet to above the jump
|
|
169
|
+
if (terrain.floorAt(gx, n.y, gz) != null || terrain.floorAt(gx, n.y + 1, gz) != null) return
|
|
170
|
+
if (!columnClear(terrain, gx, gz, n.floor, top)) return
|
|
171
|
+
if (!columnClear(terrain, n.x, n.z, n.floor + PLAYER_HEIGHT, top)) return
|
|
172
|
+
const lx = n.x + dx * (g + 1)
|
|
173
|
+
const lz = n.z + dz * (g + 1)
|
|
174
|
+
for (const y of [n.y, n.y - 1, n.y + 1]) {
|
|
175
|
+
const floor = terrain.floorAt(lx, y, lz)
|
|
176
|
+
if (floor == null) continue
|
|
177
|
+
const rise = floor - n.floor
|
|
178
|
+
if (rise > 1 + 1e-6 && g > 1) continue
|
|
179
|
+
if (rise > JUMP - 0.25 || rise < -2) continue
|
|
180
|
+
if (!terrain.bodyFits(lx, floor, lz, null) || !columnClear(terrain, lx, lz, floor, top)) continue
|
|
181
|
+
// only worth it when walking around or dropping in is not an option: the gap is deep or dangerous
|
|
182
|
+
const below = landing(terrain, gx, n.y - 1, gz, n.floor, opts.maxDropDown)
|
|
183
|
+
if (below && !below.water && n.floor - below.floor <= 1) return
|
|
184
|
+
out.push(node(lx, y, lz, floor, 'parkour', COST.walk * (g + 1) + COST.parkourGap * g, { gap: g }))
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function vertical (n, terrain, opts, out) {
|
|
191
|
+
// climbing
|
|
192
|
+
if (climbNode(terrain, n.x, n.y + 1, n.z) || (n.climb && terrain.at(n.x, n.y + 1, n.z)?.climbable)) {
|
|
193
|
+
if (terrain.bodyFits(n.x, n.y + 1, n.z, null)) out.push(node(n.x, n.y + 1, n.z, n.y + 1, 'climb', COST.climbUp, { climb: true }))
|
|
194
|
+
}
|
|
195
|
+
if (n.climb || climbNode(terrain, n.x, n.y - 1, n.z)) {
|
|
196
|
+
if (climbNode(terrain, n.x, n.y - 1, n.z)) {
|
|
197
|
+
out.push(node(n.x, n.y - 1, n.z, n.y - 1, 'climb', COST.climbDown, { climb: true }))
|
|
198
|
+
} else if (n.climb) {
|
|
199
|
+
const floor = terrain.floorAt(n.x, n.y - 1, n.z)
|
|
200
|
+
if (floor != null) out.push(node(n.x, n.y - 1, n.z, floor, 'climb', COST.climbDown))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// swimming up and down
|
|
205
|
+
if (n.water) {
|
|
206
|
+
if (waterNode(terrain, n.x, n.y + 1, n.z)) out.push(node(n.x, n.y + 1, n.z, n.y + 1, 'swim', COST.swim, { water: true }))
|
|
207
|
+
if (waterNode(terrain, n.x, n.y - 1, n.z)) out.push(node(n.x, n.y - 1, n.z, n.y - 1, 'swim', COST.swim, { water: true }))
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// digging the floor away and dropping onto the block below it
|
|
212
|
+
if (opts.canDig && !n.climb && n.floor === Math.floor(n.floor)) {
|
|
213
|
+
const below = terrain.at(n.x, n.y - 1, n.z)
|
|
214
|
+
if (!below || below.empty || !terrain.canDigAt(n.x, n.y - 1, n.z, below)) return
|
|
215
|
+
const floor = terrain.floorAt(n.x, n.y - 1, n.z)
|
|
216
|
+
const under = terrain.at(n.x, n.y - 2, n.z)
|
|
217
|
+
if (floor != null || !under || under.empty || under.danger || under.water || under.top < 1) return
|
|
218
|
+
const dig = [[n.x, n.y - 1, n.z, below]]
|
|
219
|
+
out.push(node(n.x, n.y - 1, n.z, n.y - 1, 'dig-down', COST.dropDown + digCost(terrain, dig), { dig }))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { neighbours, waterNode, climbNode }
|