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 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
@@ -25,3 +25,4 @@ module.exports.BoatPhysics = BoatPhysics
25
25
  module.exports.WorldView = WorldView
26
26
  module.exports.AABB = AABB
27
27
  module.exports.versionFlags = versionFlags
28
+ module.exports.goals = require('./lib/pathfinder/goals')
@@ -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
+ }