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
package/README.md
CHANGED
|
@@ -129,6 +129,35 @@ Added:
|
|
|
129
129
|
- `bot.spectra.world` – block and fluid lookups used by the engine
|
|
130
130
|
- event `clientLoaded` – 1.21.4+, emitted when `player_loaded` is sent
|
|
131
131
|
|
|
132
|
+
### Pathfinder
|
|
133
|
+
|
|
134
|
+
A built-in pathfinder with the same API as mineflayer-pathfinder, driven by the engine itself:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const { createBot, goals } = require('spectra-physics')
|
|
138
|
+
|
|
139
|
+
const bot = createBot({ host: 'localhost', username: 'Steve', version: '1.21.4' })
|
|
140
|
+
|
|
141
|
+
bot.once('spawn', async () => {
|
|
142
|
+
await bot.pathfinder.goto(new goals.GoalBlock(120, 64, -40))
|
|
143
|
+
bot.pathfinder.setGoal(new goals.GoalFollow(bot.nearestEntity(e => e.name === 'cow'), 2), true)
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- A* with costs in ticks, so it picks the fastest route: walking, sprinting, slabs and stairs without jumping,
|
|
148
|
+
jumps, drops, sprint-jumps over gaps, ladders and vines, swimming, and digging through, up and down
|
|
149
|
+
- keys are chosen by simulating the player a few ticks ahead with this engine: when to sprint, when to jump,
|
|
150
|
+
which corners can be cut
|
|
151
|
+
- turning goes through the mouse model; blocks are broken tick by tick like the game client (face in view,
|
|
152
|
+
progress and swing every tick, 5 ticks between blocks), also available as `bot.spectra.dig(block)`
|
|
153
|
+
- the route is planned again when blocks on it change, the goal moves or the bot gets stuck
|
|
154
|
+
- goals: `GoalBlock`, `GoalNear`, `GoalXZ`, `GoalNearXZ`, `GoalY`, `GoalGetToBlock`, `GoalLookAtBlock`,
|
|
155
|
+
`GoalFollow`, `GoalCompositeAny`, `GoalCompositeAll`, `GoalInvert`
|
|
156
|
+
- `bot.pathfinder.settings`: `canDig`, `allowParkour`, `allowLongParkour`, `allowSprinting`, `maxDropDown`,
|
|
157
|
+
`thinkTimeout`, `tickTimeout`; events `path_update`, `path_reset`, `path_stop`, `goal_reached`, `goal_updated`
|
|
158
|
+
|
|
159
|
+
mineflayer-pathfinder works too: `bot.physics.simulatePlayer` runs its look-ahead on this engine.
|
|
160
|
+
|
|
132
161
|
### Standalone engine
|
|
133
162
|
|
|
134
163
|
The engine can run without a connection, for prediction or tests:
|
|
@@ -186,6 +215,7 @@ powder snow, ice, soul sand, pushing, item use, and the position reporting thres
|
|
|
186
215
|
lib/engine world access, collisions, version switches, boat
|
|
187
216
|
lib/engine/player the player tick: input, fluids, blocks under the player, move, travel, block effects and their
|
|
188
217
|
traversal order, pose, pushing
|
|
218
|
+
lib/pathfinder goals, terrain, moves, A*, block breaking and the path follower
|
|
189
219
|
lib/client mineflayer integration: controls, teleports, server packets, position reporting, riding, item use,
|
|
190
220
|
entity interaction, registry tables, protocol fixes, the shared clock
|
|
191
221
|
bench tick cost and bit-exact output fingerprints
|
package/index.js
CHANGED
package/lib/client/index.js
CHANGED
|
@@ -15,6 +15,8 @@ const { installTicker } = require('./ticker')
|
|
|
15
15
|
const { installItemUse } = require('./items')
|
|
16
16
|
const { installInteract } = require('./interact')
|
|
17
17
|
const { installProtocolPatches } = require('./protocol')
|
|
18
|
+
const { installSimulation } = require('./simulation')
|
|
19
|
+
const { installPathfinder } = require('../pathfinder')
|
|
18
20
|
|
|
19
21
|
function debugLogger (option) {
|
|
20
22
|
if (typeof option === 'function') return option
|
|
@@ -75,6 +77,8 @@ function spectraPhysics (options = {}) {
|
|
|
75
77
|
installTeleport(ctx)
|
|
76
78
|
installServerHandlers(ctx)
|
|
77
79
|
installRiding(ctx)
|
|
80
|
+
installSimulation(ctx)
|
|
81
|
+
installPathfinder(ctx)
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// bot.physics.simulatePlayer for plugins written against prismarine-physics (mineflayer-pathfinder plans
|
|
3
|
+
// its jumps with it): the prediction runs on this engine, so plans match how the bot really moves.
|
|
4
|
+
|
|
5
|
+
const conv = require('mineflayer/lib/conversions')
|
|
6
|
+
|
|
7
|
+
function installSimulation (ctx) {
|
|
8
|
+
const { bot, player } = ctx
|
|
9
|
+
const running = new WeakMap()
|
|
10
|
+
|
|
11
|
+
bot.physics.simulatePlayer = (state) => {
|
|
12
|
+
let sim = running.get(state)
|
|
13
|
+
if (!sim) {
|
|
14
|
+
sim = player.clone()
|
|
15
|
+
running.set(state, sim)
|
|
16
|
+
}
|
|
17
|
+
sim.pos = { x: state.pos.x, y: state.pos.y, z: state.pos.z }
|
|
18
|
+
sim.vel = { x: state.vel.x, y: state.vel.y, z: state.vel.z }
|
|
19
|
+
sim.onGround = !!state.onGround
|
|
20
|
+
sim.yRot = conv.toNotchianYaw(state.yaw)
|
|
21
|
+
sim.xRot = conv.toNotchianPitch(state.pitch)
|
|
22
|
+
const c = state.control ?? {}
|
|
23
|
+
sim.tick({ forward: !!c.forward, back: !!c.back, left: !!c.left, right: !!c.right, jump: !!c.jump, sprint: !!c.sprint, sneak: !!c.sneak })
|
|
24
|
+
state.pos.set(sim.pos.x, sim.pos.y, sim.pos.z)
|
|
25
|
+
state.vel.set(sim.vel.x, sim.vel.y, sim.vel.z)
|
|
26
|
+
state.onGround = sim.onGround
|
|
27
|
+
state.isInWater = sim.isInWater()
|
|
28
|
+
state.isInLava = sim.isInLava()
|
|
29
|
+
state.isCollidedHorizontally = sim.horizontalCollision
|
|
30
|
+
state.isCollidedVertically = sim.verticalCollision
|
|
31
|
+
return state
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { installSimulation }
|
|
@@ -79,6 +79,17 @@ class PlayerPhysics {
|
|
|
79
79
|
this.events = { abilitiesChanged: false }
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// an independent copy for looking ahead: same world and environment, own state
|
|
83
|
+
clone () {
|
|
84
|
+
const copy = new PlayerPhysics(this.world, this.V, this.env)
|
|
85
|
+
for (const key of Object.keys(this)) {
|
|
86
|
+
if (key === 'world' || key === 'V' || key === 'env' || key === 'collisionContext') continue
|
|
87
|
+
const value = this[key]
|
|
88
|
+
copy[key] = value !== null && typeof value === 'object' ? structuredClone(value) : value
|
|
89
|
+
}
|
|
90
|
+
return copy
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
height () { return pose.POSES[this.pose].height }
|
|
83
94
|
eyeHeight () { return pose.POSES[this.pose].eye }
|
|
84
95
|
bbAt (x, y, z) { return pose.boxFor(this.pose, x, y, z) }
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// A* over the move graph. The search can be spread over several ticks (compute() returns 'partial' while
|
|
3
|
+
// it is still thinking) and always offers the best path found so far.
|
|
4
|
+
|
|
5
|
+
const { performance } = require('perf_hooks')
|
|
6
|
+
const { neighbours } = require('./moves')
|
|
7
|
+
|
|
8
|
+
// Plain A* (optimal) while the search converges. When it keeps expanding without reaching the goal, as when
|
|
9
|
+
// digging makes the real cost far higher than any estimate, the heuristic gets more weight step by step and
|
|
10
|
+
// the search turns greedy enough to find a route in time.
|
|
11
|
+
const RELAX_EVERY = 2000
|
|
12
|
+
const RELAX_FACTOR = 2
|
|
13
|
+
const MAX_WEIGHT = 10000
|
|
14
|
+
|
|
15
|
+
const OFFSET = 2 ** 19
|
|
16
|
+
const SPAN = 2 ** 20
|
|
17
|
+
|
|
18
|
+
class Heap {
|
|
19
|
+
constructor () { this.items = [] }
|
|
20
|
+
get size () { return this.items.length }
|
|
21
|
+
|
|
22
|
+
push (n) {
|
|
23
|
+
const a = this.items
|
|
24
|
+
a.push(n)
|
|
25
|
+
let i = a.length - 1
|
|
26
|
+
n.heapIndex = i
|
|
27
|
+
while (i > 0) {
|
|
28
|
+
const p = (i - 1) >> 1
|
|
29
|
+
if (!less(a[i], a[p])) break
|
|
30
|
+
swap(a, i, p)
|
|
31
|
+
i = p
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pop () {
|
|
36
|
+
const a = this.items
|
|
37
|
+
const top = a[0]
|
|
38
|
+
const last = a.pop()
|
|
39
|
+
if (a.length) {
|
|
40
|
+
a[0] = last
|
|
41
|
+
last.heapIndex = 0
|
|
42
|
+
this.down(0)
|
|
43
|
+
}
|
|
44
|
+
top.heapIndex = -1
|
|
45
|
+
return top
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
update (n) {
|
|
49
|
+
const a = this.items
|
|
50
|
+
let i = n.heapIndex
|
|
51
|
+
while (i > 0) {
|
|
52
|
+
const p = (i - 1) >> 1
|
|
53
|
+
if (!less(a[i], a[p])) break
|
|
54
|
+
swap(a, i, p)
|
|
55
|
+
i = p
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
down (i) {
|
|
60
|
+
const a = this.items
|
|
61
|
+
for (;;) {
|
|
62
|
+
const l = 2 * i + 1
|
|
63
|
+
const r = l + 1
|
|
64
|
+
let m = i
|
|
65
|
+
if (l < a.length && less(a[l], a[m])) m = l
|
|
66
|
+
if (r < a.length && less(a[r], a[m])) m = r
|
|
67
|
+
if (m === i) return
|
|
68
|
+
swap(a, i, m)
|
|
69
|
+
i = m
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const less = (a, b) => a.f < b.f || (a.f === b.f && a.h < b.h)
|
|
75
|
+
function swap (a, i, j) {
|
|
76
|
+
const t = a[i]; a[i] = a[j]; a[j] = t
|
|
77
|
+
a[i].heapIndex = i; a[j].heapIndex = j
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class AStar {
|
|
81
|
+
constructor (start, goal, terrain, opts) {
|
|
82
|
+
this.goal = goal
|
|
83
|
+
this.terrain = terrain
|
|
84
|
+
this.opts = opts
|
|
85
|
+
this.ox = start.x
|
|
86
|
+
this.oz = start.z
|
|
87
|
+
this.nodes = new Map()
|
|
88
|
+
this.open = new Heap()
|
|
89
|
+
this.started = performance.now()
|
|
90
|
+
this.visited = 0
|
|
91
|
+
this.weight = 1
|
|
92
|
+
const first = this.record(start)
|
|
93
|
+
first.g = 0
|
|
94
|
+
first.h = goal.heuristic(start)
|
|
95
|
+
first.f = first.h
|
|
96
|
+
this.best = first
|
|
97
|
+
this.open.push(first)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
key (x, y, z) {
|
|
101
|
+
return ((x - this.ox + OFFSET) * SPAN + (z - this.oz + OFFSET)) * 8192 + (y + 4096)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
record (n) {
|
|
105
|
+
const key = this.key(n.x, n.y, n.z)
|
|
106
|
+
let r = this.nodes.get(key)
|
|
107
|
+
if (!r) {
|
|
108
|
+
r = { ...n, g: Infinity, h: 0, f: Infinity, parent: null, heapIndex: -1, closed: false }
|
|
109
|
+
this.nodes.set(key, r)
|
|
110
|
+
}
|
|
111
|
+
return r
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// runs until the goal is found, the tick budget is spent, or the search gives up
|
|
115
|
+
compute () {
|
|
116
|
+
const { tickTimeout, thinkTimeout, maxNodes } = this.opts
|
|
117
|
+
const tickStart = performance.now()
|
|
118
|
+
const goal = this.goal
|
|
119
|
+
while (this.open.size) {
|
|
120
|
+
if (performance.now() - tickStart > tickTimeout) return this.result('partial')
|
|
121
|
+
if (performance.now() - this.started > thinkTimeout || this.visited > maxNodes) return this.result('timeout')
|
|
122
|
+
if (this.visited > 0 && this.visited % RELAX_EVERY === 0 && this.weight < MAX_WEIGHT) this.relax()
|
|
123
|
+
const cur = this.open.pop()
|
|
124
|
+
if (goal.isEnd(cur)) return this.result('success', cur)
|
|
125
|
+
cur.closed = true
|
|
126
|
+
this.visited++
|
|
127
|
+
for (const nb of neighbours(cur, this.terrain, this.opts)) {
|
|
128
|
+
const r = this.record(nb)
|
|
129
|
+
if (r.closed) continue
|
|
130
|
+
const g = cur.g + nb.cost
|
|
131
|
+
if (g >= r.g) continue
|
|
132
|
+
Object.assign(r, nb)
|
|
133
|
+
r.parent = cur
|
|
134
|
+
r.g = g
|
|
135
|
+
r.h = goal.heuristic(r)
|
|
136
|
+
r.f = g + r.h * this.weight
|
|
137
|
+
if (r.h < this.best.h || (r.h === this.best.h && g < this.best.g)) this.best = r
|
|
138
|
+
if (r.heapIndex < 0) this.open.push(r)
|
|
139
|
+
else this.open.update(r)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return this.result('noPath')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
relax () {
|
|
146
|
+
this.weight = Math.min(MAX_WEIGHT, this.weight * RELAX_FACTOR)
|
|
147
|
+
const items = this.open.items
|
|
148
|
+
for (const n of items) n.f = n.g + n.h * this.weight
|
|
149
|
+
for (let i = (items.length >> 1) - 1; i >= 0; i--) this.open.down(i)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
result (status, end = this.best) {
|
|
153
|
+
const path = []
|
|
154
|
+
for (let n = end; n && n.parent; n = n.parent) path.push(n)
|
|
155
|
+
path.reverse()
|
|
156
|
+
return {
|
|
157
|
+
status,
|
|
158
|
+
path,
|
|
159
|
+
cost: end.g,
|
|
160
|
+
time: performance.now() - this.started,
|
|
161
|
+
visitedNodes: this.visited,
|
|
162
|
+
generatedNodes: this.nodes.size
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { AStar }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Move costs in game ticks, from the engine's own speeds: sprinting 0.2806 blocks per tick, climbing
|
|
3
|
+
// 0.1176 up and 0.15 down, swimming about 0.11. A* then finds the fastest route, not just the shortest.
|
|
4
|
+
|
|
5
|
+
const walk = 1 / 0.2806
|
|
6
|
+
|
|
7
|
+
const COST = {
|
|
8
|
+
walk,
|
|
9
|
+
diagonal: walk * Math.SQRT2,
|
|
10
|
+
jumpUp: walk + 2.5, // the jump costs a few ticks of forward speed
|
|
11
|
+
dropDown: walk + 1, // plus fallTick per block fallen
|
|
12
|
+
fallTick: 1.2,
|
|
13
|
+
parkourGap: 2, // extra for every jump over a gap, on top of the distance
|
|
14
|
+
climbUp: 1 / 0.1176,
|
|
15
|
+
climbDown: 1 / 0.15,
|
|
16
|
+
swim: 9,
|
|
17
|
+
digSetup: 6, // stopping and turning to the block
|
|
18
|
+
avoid: 40 // stepping somewhere unpleasant but harmless (soul sand, honey, a cobweb we must cross)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { COST }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Block breaking the way MultiPlayerGameMode does it: look at a visible face, START on one tick, add the
|
|
3
|
+
// destroy progress and swing every tick, STOP on the tick it reaches 1, then wait 5 ticks before the next
|
|
4
|
+
// block. mineflayer's dig() runs on a wall clock and swings every 350 ms instead.
|
|
5
|
+
|
|
6
|
+
const { Vec3 } = require('vec3')
|
|
7
|
+
|
|
8
|
+
const DESTROY_DELAY = 5
|
|
9
|
+
const FACES = [
|
|
10
|
+
{ axis: 'y', sign: -1, face: 0 }, { axis: 'y', sign: 1, face: 1 },
|
|
11
|
+
{ axis: 'z', sign: -1, face: 2 }, { axis: 'z', sign: 1, face: 3 },
|
|
12
|
+
{ axis: 'x', sign: -1, face: 4 }, { axis: 'x', sign: 1, face: 5 }
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function installDigger (ctx) {
|
|
16
|
+
const { bot, client, V } = ctx
|
|
17
|
+
const hasSequence = V.protocol >= 759
|
|
18
|
+
let sequence = 0
|
|
19
|
+
let job = null
|
|
20
|
+
let delay = 0
|
|
21
|
+
|
|
22
|
+
const send = (status, block, face) => {
|
|
23
|
+
const packet = { status, location: block.position, face }
|
|
24
|
+
if (hasSequence) packet.sequence = status === 0 || status === 2 ? ++sequence : 0
|
|
25
|
+
client.write('block_dig', packet)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// faces the eyes can see, aimed at through the open side; closest first
|
|
29
|
+
function aimPoints (block) {
|
|
30
|
+
const eye = bot.entity.position.offset(0, bot.entity.eyeHeight ?? 1.62, 0)
|
|
31
|
+
const c = block.position.offset(0.5, 0.5, 0.5)
|
|
32
|
+
const out = []
|
|
33
|
+
for (const f of FACES) {
|
|
34
|
+
const toward = f.sign * (eye[f.axis] - c[f.axis])
|
|
35
|
+
if (toward <= 0.5) continue
|
|
36
|
+
const point = c.clone()
|
|
37
|
+
point[f.axis] += f.sign * 0.5
|
|
38
|
+
const dir = point.minus(eye).normalize()
|
|
39
|
+
const hit = bot.world.raycast(eye, dir, 6)
|
|
40
|
+
if (!hit || !hit.position.equals(block.position)) continue
|
|
41
|
+
out.push({ point: hit.intersect ?? point, face: hit.face ?? f.face, distance: eye.distanceTo(point) })
|
|
42
|
+
}
|
|
43
|
+
return out.sort((a, b) => a.distance - b.distance)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function equipBest (block) {
|
|
47
|
+
const tool = bestTool(bot, block)
|
|
48
|
+
if (tool && bot.heldItem?.type !== tool.type) await bot.equip(tool, 'hand')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bot.on('physicsTick', () => {
|
|
52
|
+
if (delay > 0) delay--
|
|
53
|
+
if (!job) return
|
|
54
|
+
const block = bot.blockAt(job.block.position)
|
|
55
|
+
if (!block || block.type !== job.block.type) return finish(null)
|
|
56
|
+
if (!job.started) {
|
|
57
|
+
if (delay > 0) return
|
|
58
|
+
job.started = true
|
|
59
|
+
const ticks = Math.ceil(bot.digTime(block) / 50)
|
|
60
|
+
send(0, block, job.face)
|
|
61
|
+
bot.swingArm()
|
|
62
|
+
if (ticks <= 1) return finish(null, true)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (job.waitingBreak) {
|
|
66
|
+
if (++job.waited > 20) finish(new Error('the server did not break the block'))
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
job.progress += 50 / Math.max(50, bot.digTime(block))
|
|
70
|
+
bot.swingArm()
|
|
71
|
+
if (job.progress >= 1 - 1e-9) {
|
|
72
|
+
send(2, block, job.face)
|
|
73
|
+
delay = DESTROY_DELAY
|
|
74
|
+
job.waitingBreak = true
|
|
75
|
+
job.waited = 0
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
bot.on('blockUpdate', (oldBlock, newBlock) => {
|
|
80
|
+
if (!job || !newBlock || !newBlock.position.equals(job.block.position)) return
|
|
81
|
+
if (newBlock.type !== job.block.type) finish(null)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
function finish (err) {
|
|
85
|
+
const j = job
|
|
86
|
+
job = null
|
|
87
|
+
if (!j) return
|
|
88
|
+
if (err) j.reject(err)
|
|
89
|
+
else j.resolve()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx.dig = async (block) => {
|
|
93
|
+
if (job) throw new Error('already digging')
|
|
94
|
+
const aim = aimPoints(block)[0]
|
|
95
|
+
if (!aim) throw new Error(`${block.name} is not in view`)
|
|
96
|
+
await bot.lookAt(aim.point)
|
|
97
|
+
await equipBest(block)
|
|
98
|
+
if (bot.digTime(block) === Infinity) throw new Error(`cannot dig ${block.name}`)
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
job = { block, face: aim.face, progress: 0, started: false, resolve, reject }
|
|
101
|
+
setTimeout(() => { if (job && job.block === block) finish(new Error('dig timeout')) }, 60000)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
ctx.stopDigging = () => {
|
|
106
|
+
if (!job) return
|
|
107
|
+
if (job.started && !job.waitingBreak) send(1, job.block, job.face)
|
|
108
|
+
finish(new Error('digging stopped'))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ctx.isDigging = () => !!job
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// the inventory item that breaks a block fastest (null: the empty hand is as good)
|
|
115
|
+
function bestTool (bot, block) {
|
|
116
|
+
let best = null
|
|
117
|
+
let bestTime = bot.digTime ? digTimeWith(bot, block, null) : Infinity
|
|
118
|
+
for (const item of bot.inventory.items()) {
|
|
119
|
+
const t = digTimeWith(bot, block, item)
|
|
120
|
+
if (t < bestTime) { best = item; bestTime = t }
|
|
121
|
+
}
|
|
122
|
+
return best
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function digTimeWith (bot, block, item) {
|
|
126
|
+
let enchants = []
|
|
127
|
+
try { enchants = item?.enchants ?? [] } catch { enchants = [] }
|
|
128
|
+
return block.digTime(item ? item.type : null, bot.game?.gameMode === 'creative', bot.entity.isInWater, !bot.entity.onGround,
|
|
129
|
+
Array.isArray(enchants) ? enchants : [], bot.entity.effects)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { installDigger, bestTool, Vec3 }
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// Goals for the pathfinder, with the same shape as mineflayer-pathfinder's (heuristic, isEnd, hasChanged,
|
|
3
|
+
// isValid) so code written for it keeps working. Heuristics are in ticks, matching the move costs: the
|
|
4
|
+
// fastest way over flat ground is sprinting (about 3.56 ticks per block), so they never overestimate.
|
|
5
|
+
|
|
6
|
+
const { COST } = require('./costs')
|
|
7
|
+
|
|
8
|
+
function horizontal (dx, dz) {
|
|
9
|
+
const ax = Math.abs(dx)
|
|
10
|
+
const az = Math.abs(dz)
|
|
11
|
+
return Math.max(ax, az) + (Math.SQRT2 - 1) * Math.min(ax, az)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// lower bound on the ticks needed to cover the offset: going up is at least a walk per block,
|
|
15
|
+
// going down at least the fall time per block
|
|
16
|
+
function estimate (dx, dy, dz) {
|
|
17
|
+
return COST.walk * Math.max(horizontal(dx, dz), Math.max(0, dy)) + COST.fallTick * Math.max(0, -dy)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class Goal {
|
|
21
|
+
heuristic () { return 0 }
|
|
22
|
+
isEnd () { return true }
|
|
23
|
+
hasChanged () { return false }
|
|
24
|
+
isValid () { return true }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class GoalBlock extends Goal {
|
|
28
|
+
constructor (x, y, z) {
|
|
29
|
+
super()
|
|
30
|
+
this.x = Math.floor(x); this.y = Math.floor(y); this.z = Math.floor(z)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
heuristic (node) { return estimate(this.x - node.x, this.y - node.y, this.z - node.z) }
|
|
34
|
+
isEnd (node) { return node.x === this.x && node.y === this.y && node.z === this.z }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class GoalNear extends Goal {
|
|
38
|
+
constructor (x, y, z, range) {
|
|
39
|
+
super()
|
|
40
|
+
this.x = Math.floor(x); this.y = Math.floor(y); this.z = Math.floor(z)
|
|
41
|
+
this.rangeSq = range * range
|
|
42
|
+
this.range = range
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
heuristic (node) {
|
|
46
|
+
const d = estimate(this.x - node.x, this.y - node.y, this.z - node.z)
|
|
47
|
+
return Math.max(0, d - this.range * COST.walk)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isEnd (node) {
|
|
51
|
+
const dx = this.x - node.x; const dy = this.y - node.y; const dz = this.z - node.z
|
|
52
|
+
return dx * dx + dy * dy + dz * dz <= this.rangeSq
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class GoalXZ extends Goal {
|
|
57
|
+
constructor (x, z) {
|
|
58
|
+
super()
|
|
59
|
+
this.x = Math.floor(x); this.z = Math.floor(z)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
heuristic (node) { return COST.walk * horizontal(this.x - node.x, this.z - node.z) }
|
|
63
|
+
isEnd (node) { return node.x === this.x && node.z === this.z }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class GoalNearXZ extends Goal {
|
|
67
|
+
constructor (x, z, range) {
|
|
68
|
+
super()
|
|
69
|
+
this.x = Math.floor(x); this.z = Math.floor(z)
|
|
70
|
+
this.range = range
|
|
71
|
+
this.rangeSq = range * range
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
heuristic (node) { return Math.max(0, COST.walk * (horizontal(this.x - node.x, this.z - node.z) - this.range)) }
|
|
75
|
+
isEnd (node) {
|
|
76
|
+
const dx = this.x - node.x; const dz = this.z - node.z
|
|
77
|
+
return dx * dx + dz * dz <= this.rangeSq
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class GoalY extends Goal {
|
|
82
|
+
constructor (y) {
|
|
83
|
+
super()
|
|
84
|
+
this.y = Math.floor(y)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
heuristic (node) { return estimate(0, this.y - node.y, 0) }
|
|
88
|
+
isEnd (node) { return node.y === this.y }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// stand next to a block (any of the 6 neighbours, or on top of it)
|
|
92
|
+
class GoalGetToBlock extends Goal {
|
|
93
|
+
constructor (x, y, z) {
|
|
94
|
+
super()
|
|
95
|
+
this.x = Math.floor(x); this.y = Math.floor(y); this.z = Math.floor(z)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
heuristic (node) {
|
|
99
|
+
const dx = this.x - node.x; const dy = this.y - node.y; const dz = this.z - node.z
|
|
100
|
+
return Math.max(0, estimate(dx, dy, dz) - COST.walk)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
isEnd (node) {
|
|
104
|
+
return Math.abs(this.x - node.x) + Math.abs(this.y - node.y) + Math.abs(this.z - node.z) === 1
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// a face of the block can be seen and reached from the eyes (survival 4.5 blocks)
|
|
109
|
+
class GoalLookAtBlock extends Goal {
|
|
110
|
+
constructor (pos, world, options = {}) {
|
|
111
|
+
super()
|
|
112
|
+
this.x = Math.floor(pos.x); this.y = Math.floor(pos.y); this.z = Math.floor(pos.z)
|
|
113
|
+
this.world = world
|
|
114
|
+
this.reach = options.reach ?? 4.5
|
|
115
|
+
this.eyeHeight = options.eyeHeight ?? 1.62
|
|
116
|
+
this.reachSq = this.reach * this.reach
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
visible (node) {
|
|
120
|
+
if (!this.world?.raycast) return true
|
|
121
|
+
const { Vec3 } = require('vec3')
|
|
122
|
+
const eye = new Vec3(node.x + 0.5, (node.floor ?? node.y) + this.eyeHeight, node.z + 0.5)
|
|
123
|
+
const faces = [[0.5, 1.001, 0.5], [0.5, -0.001, 0.5], [-0.001, 0.5, 0.5], [1.001, 0.5, 0.5], [0.5, 0.5, -0.001], [0.5, 0.5, 1.001]]
|
|
124
|
+
for (const [fx, fy, fz] of faces) {
|
|
125
|
+
const target = new Vec3(this.x + fx, this.y + fy, this.z + fz)
|
|
126
|
+
const dir = target.minus(eye)
|
|
127
|
+
const length = dir.norm()
|
|
128
|
+
if (length > this.reach) continue
|
|
129
|
+
const hit = this.world.raycast(eye, dir.normalize(), length + 0.01)
|
|
130
|
+
if (hit && hit.position.x === this.x && hit.position.y === this.y && hit.position.z === this.z) return true
|
|
131
|
+
}
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
heuristic (node) {
|
|
136
|
+
const d = estimate(this.x - node.x, this.y - node.y, this.z - node.z)
|
|
137
|
+
return Math.max(0, d - (this.reach - 1) * COST.walk)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isEnd (node) {
|
|
141
|
+
const dx = this.x + 0.5 - (node.x + 0.5)
|
|
142
|
+
const dy = this.y + 0.5 - (node.y + this.eyeHeight)
|
|
143
|
+
const dz = this.z + 0.5 - (node.z + 0.5)
|
|
144
|
+
if (node.x === this.x && node.z === this.z && (node.y === this.y || node.y + 1 === this.y)) return false
|
|
145
|
+
return dx * dx + dy * dy + dz * dz <= this.reachSq && this.visible(node)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
class GoalCompositeAny extends Goal {
|
|
150
|
+
constructor (goals = []) {
|
|
151
|
+
super()
|
|
152
|
+
this.goals = goals
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
push (goal) { this.goals.push(goal) }
|
|
156
|
+
heuristic (node) { return Math.min(...this.goals.map(g => g.heuristic(node))) }
|
|
157
|
+
isEnd (node) { return this.goals.some(g => g.isEnd(node)) }
|
|
158
|
+
hasChanged () { return this.goals.some(g => g.hasChanged()) }
|
|
159
|
+
isValid () { return this.goals.every(g => g.isValid()) }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
class GoalCompositeAll extends Goal {
|
|
163
|
+
constructor (goals = []) {
|
|
164
|
+
super()
|
|
165
|
+
this.goals = goals
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
push (goal) { this.goals.push(goal) }
|
|
169
|
+
heuristic (node) { return Math.max(...this.goals.map(g => g.heuristic(node))) }
|
|
170
|
+
isEnd (node) { return this.goals.every(g => g.isEnd(node)) }
|
|
171
|
+
hasChanged () { return this.goals.some(g => g.hasChanged()) }
|
|
172
|
+
isValid () { return this.goals.every(g => g.isValid()) }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class GoalInvert extends Goal {
|
|
176
|
+
constructor (goal) {
|
|
177
|
+
super()
|
|
178
|
+
this.goal = goal
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
heuristic (node) { return -this.goal.heuristic(node) }
|
|
182
|
+
isEnd (node) { return !this.goal.isEnd(node) }
|
|
183
|
+
hasChanged () { return this.goal.hasChanged() }
|
|
184
|
+
isValid () { return this.goal.isValid() }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// stay within range of an entity; the path is refreshed when it moves away from where it was planned to
|
|
188
|
+
class GoalFollow extends Goal {
|
|
189
|
+
constructor (entity, range) {
|
|
190
|
+
super()
|
|
191
|
+
this.entity = entity
|
|
192
|
+
this.range = range
|
|
193
|
+
this.rangeSq = range * range
|
|
194
|
+
this.update()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
update () {
|
|
198
|
+
const p = this.entity.position
|
|
199
|
+
this.x = Math.floor(p.x); this.y = Math.floor(p.y); this.z = Math.floor(p.z)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
heuristic (node) {
|
|
203
|
+
const d = estimate(this.x - node.x, this.y - node.y, this.z - node.z)
|
|
204
|
+
return Math.max(0, d - this.range * COST.walk)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
isEnd (node) {
|
|
208
|
+
const dx = this.x - node.x; const dy = this.y - node.y; const dz = this.z - node.z
|
|
209
|
+
return dx * dx + dy * dy + dz * dz <= this.rangeSq
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
hasChanged () {
|
|
213
|
+
const p = this.entity.position
|
|
214
|
+
const x = Math.floor(p.x); const y = Math.floor(p.y); const z = Math.floor(p.z)
|
|
215
|
+
if (x === this.x && y === this.y && z === this.z) return false
|
|
216
|
+
this.x = x; this.y = y; this.z = z
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
isValid () { return this.entity != null && this.entity.isValid !== false }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
Goal,
|
|
225
|
+
GoalBlock,
|
|
226
|
+
GoalNear,
|
|
227
|
+
GoalXZ,
|
|
228
|
+
GoalNearXZ,
|
|
229
|
+
GoalY,
|
|
230
|
+
GoalGetToBlock,
|
|
231
|
+
GoalLookAtBlock,
|
|
232
|
+
GoalCompositeAny,
|
|
233
|
+
GoalCompositeAll,
|
|
234
|
+
GoalInvert,
|
|
235
|
+
GoalFollow
|
|
236
|
+
}
|