diff --git a/docs/API.md b/docs/API.md
index cc4f1a6..73115ed 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -25,6 +25,7 @@
- [GoalCompositeAny](#goalcompositeany)
- [GoalCompositeAll](#goalcompositeall)
- [Settings](#settings)
+- [Exclusion Zones](#exclusion-zones)
- [Events](#events)
- [pathGenerated](#pathGenerated)
- [goalSet](#goalSet)
@@ -560,6 +561,9 @@ These are the currently available settings.
| `infiniteLiquidDropdownDistance` | `boolean` | Whether or not to have an infinite liquid dropdown distance. | `true` |
| `allowSprinting` | `boolean` | Whether or not to allow sprinting. | `true` |
| `careAboutLookAlignment` | `boolean` | Whether or not to care about look alignment. | `true` |
+| `exclusionAreasStep` | `ExclusionArea[]` | "Keep out" rules for blocks the bot would **stand in**. See [Exclusion Zones](#exclusion-zones). | `[]` |
+| `exclusionAreasBreak` | `ExclusionArea[]` | "Keep out" rules for blocks the bot would **break** (mine). | `[]` |
+| `exclusionAreasPlace` | `ExclusionArea[]` | "Keep out" rules for blocks the bot would **place** (build on). | `[]` |
```ts
@@ -583,12 +587,64 @@ interface MovementOptions {
infiniteLiquidDropdownDistance: boolean
allowSprinting: boolean
careAboutLookAlignment: boolean
+
+ movementTimeoutMs: number
+
+ // "Keep out" zones. Empty by default. See the Exclusion Zones section below.
+ exclusionAreasStep: ExclusionArea[]
+ exclusionAreasBreak: ExclusionArea[]
+ exclusionAreasPlace: ExclusionArea[]
}
```
+
Exclusion Zones
+
+Exclusion zones mark areas as off-limits. An exclusion area is a function
+returning the extra cost of a block; the same idea as upstream
+[`PrismarineJS/mineflayer-pathfinder`](https://github.com/PrismarineJS/mineflayer-pathfinder).
+
+```ts
+type ExclusionArea = (block: BlockInfo) => number
+```
+
+- `0` — no opinion.
+- a positive number — soft zone: allowed, but avoided when a cheaper route exists.
+- `Infinity` (`>= COST_INF`) — hard zone: never used.
+
+Areas live in three settings; each sums the cost of every function in its list:
+
+| Setting | Checked on every block the bot would… |
+| --- | --- |
+| `exclusionAreasStep` | stand in (the foot block of each move — walk, jump, drop, parkour, tower; optimized paths included). |
+| `exclusionAreasBreak` | break (mine). |
+| `exclusionAreasPlace` | place (build on). |
+
+Empty by default, so there is zero overhead when unused. The library ships no
+ready-made shapes — write your own. Box/radius helpers are in
+[`examples/exclusionZones.js`](../examples/exclusionZones.js).
+
+```ts
+// Never break or place inside a protected box; soft-avoid stepping on farmland.
+const farmlandId = bot.registry.blocksByName.farmland.id
+const inBox = (block) =>
+ block.position.x >= -10 && block.position.x <= 10 &&
+ block.position.z >= -10 && block.position.z <= 10 ? Infinity : 0
+
+bot.pathfinder.setMoveOptions({
+ exclusionAreasStep: [(block) => block.type === farmlandId ? 100 : 0],
+ exclusionAreasBreak: [inBox],
+ exclusionAreasPlace: [inBox]
+})
+```
+
+`exclusionAreasStep` is checked on the block the bot's **feet** land in; extend a
+box one block lower if you also need the head kept out.
+
+
+
Events
diff --git a/docs/AdvancedUsage.md b/docs/AdvancedUsage.md
index 6b20fe2..0d8691d 100644
--- a/docs/AdvancedUsage.md
+++ b/docs/AdvancedUsage.md
@@ -9,6 +9,7 @@
- [Custom Movement Providers](#custom-movement-providers)
- [Custom Movement Executors](#custom-movement-executors)
- [Custom Movement Optimizers](#custom-movement-optimizers)
+- [Exclusion Zones (Keep-Out Areas)](#exclusion-zones-keep-out-areas)
@@ -311,3 +312,37 @@ class MyMovementOptimizer extends MovementOptimizer {
}
}
```
+
+Exclusion Zones (Keep-Out Areas)
+
+To keep the bot out of an area without writing a custom `MovementProvider`, add
+an **exclusion area** to the move settings. It is a function returning the extra
+cost of a block: `0` (no opinion), a positive number (soft — avoided when a
+cheaper route exists), or `Infinity` (hard — never used).
+
+Three lists, each summing the cost of every function in it:
+
+- `exclusionAreasStep` — blocks the bot would stand in.
+- `exclusionAreasBreak` — blocks the bot would break (mine).
+- `exclusionAreasPlace` — blocks the bot would place (build on).
+
+All empty by default (no overhead). Any `(block) => number` works:
+
+```ts
+// Never mine valuable ores or anything below y=0.
+const diamondId = bot.registry.blocksByName.diamond_ore.id
+const protectOres = (block) =>
+ (block.type === diamondId || block.position.y < 0) ? Infinity : 0
+
+bot.pathfinder.setMoveOptions({ exclusionAreasBreak: [protectOres] })
+```
+
+Notes:
+
+- Costs from multiple areas in a list add up.
+- `exclusionAreasStep` is checked on the foot block of every move (walk, jump,
+ drop, parkour, tower), and optimized/straight-lined paths are blocked from
+ cutting through hard zones too.
+- Pass a fresh array to `setMoveOptions({ ... })` rather than mutating the
+ existing one.
+- Ready-to-copy box/radius helpers live in `examples/exclusionZones.js`.
diff --git a/examples/README.md b/examples/README.md
index 61bed6e..aaa1e16 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -2,3 +2,4 @@
`basic.js` shows off the basic functionality of this pathfinder, while `example.js` goes into more depth.
`bridging/bridge.ts` is the bridge demo, and `neos/neo.ts` is a neo-jump-focused variant based on the same bot setup.
+`exclusionZones.js` shows how to add "keep out" areas (exclusion zones) to the movement settings.
diff --git a/examples/exclusionZones.js b/examples/exclusionZones.js
new file mode 100644
index 0000000..3bf20aa
--- /dev/null
+++ b/examples/exclusionZones.js
@@ -0,0 +1,122 @@
+'use strict'
+
+// ---------------------------------------------------------------------------
+// Exclusion zones example
+//
+// An "exclusion area" is just a function (block) => number that returns the
+// extra cost of using a block: 0 = fine, a positive number = soft avoid,
+// Infinity = hard "keep out". The library ships no ready-made shapes on
+// purpose, so the small box/radius builders below are yours to copy and adapt.
+//
+// They go into three movement settings:
+// exclusionAreasStep -> blocks the bot may stand in / walk into
+// exclusionAreasBreak -> blocks the bot may break (mine)
+// exclusionAreasPlace -> blocks the bot may place (build on)
+//
+// Run a local server, then: node examples/exclusionZones.js
+// In chat: "goto ", "zones on", "zones off".
+// ---------------------------------------------------------------------------
+
+const { createBot } = require('mineflayer')
+const { Vec3 } = require('vec3')
+const { createPlugin, goals } = require('../dist')
+
+const { GoalBlock } = goals
+
+// --- copy these helpers into your own project ------------------------------
+
+// A box between two opposite corners (inclusive, any order).
+function boxExclusion (corner1, corner2, cost = Infinity) {
+ const minX = Math.min(corner1.x, corner2.x)
+ const minY = Math.min(corner1.y, corner2.y)
+ const minZ = Math.min(corner1.z, corner2.z)
+ const maxX = Math.max(corner1.x, corner2.x)
+ const maxY = Math.max(corner1.y, corner2.y)
+ const maxZ = Math.max(corner1.z, corner2.z)
+ return (block) => {
+ const p = block.position
+ const inside =
+ p.x >= minX && p.x <= maxX &&
+ p.y >= minY && p.y <= maxY &&
+ p.z >= minZ && p.z <= maxZ
+ return inside ? cost : 0
+ }
+}
+
+// A ball (sphere) of the given radius around a center point.
+function radiusExclusion (center, radius, cost = Infinity) {
+ const r2 = radius * radius
+ return (block) => {
+ const dx = block.position.x - center.x
+ const dy = block.position.y - center.y
+ const dz = block.position.z - center.z
+ return dx * dx + dy * dy + dz * dz <= r2 ? cost : 0
+ }
+}
+
+// ---------------------------------------------------------------------------
+
+const bot = createBot({
+ username: 'exclusion-demo',
+ auth: 'offline',
+ host: 'localhost',
+ port: 25565
+})
+
+bot.loadPlugin(createPlugin())
+
+// Example zones (tweak the coordinates to match your world):
+const noGoBox = boxExclusion(new Vec3(-8, 60, -8), new Vec3(8, 80, 8)) // hard
+const softBall = radiusExclusion(new Vec3(30, 64, 30), 6, 50) // soft
+
+function enableZones () {
+ bot.pathfinder.setMoveOptions({
+ exclusionAreasStep: [noGoBox, softBall]
+ })
+ bot.chat('Exclusion zones: ON')
+}
+
+function disableZones () {
+ bot.pathfinder.setMoveOptions({
+ exclusionAreasStep: [],
+ exclusionAreasBreak: [],
+ exclusionAreasPlace: []
+ })
+ bot.chat('Exclusion zones: OFF')
+}
+
+bot.once('spawn', () => {
+ enableZones()
+ bot.chat('Ready. Try: "goto ", "zones on", "zones off".')
+})
+
+bot.on('chat', async (username, message) => {
+ if (username === bot.username) return
+
+ const [cmd, ...args] = message.trim().split(/\s+/)
+
+ if (cmd === 'zones') {
+ if (args[0] === 'off') disableZones()
+ else enableZones()
+ return
+ }
+
+ if (cmd === 'goto') {
+ const [x, y, z] = args.map(Number)
+ if ([x, y, z].some(Number.isNaN)) {
+ bot.chat('Usage: goto ')
+ return
+ }
+
+ bot.chat(`Heading to ${x} ${y} ${z}, avoiding the zones...`)
+ try {
+ await bot.pathfinder.goto(new GoalBlock(x, y, z))
+ bot.chat('Arrived!')
+ } catch (err) {
+ bot.chat(`Could not get there: ${err.message}`)
+ }
+ }
+})
+
+bot.on('kicked', console.log)
+bot.on('error', console.log)
diff --git a/package.json b/package.json
index b07f39d..12a3178 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,9 @@
},
"ts-standard": {
"ignore": [
- "examples"
+ "examples",
+ "tests",
+ "scripts"
]
}
}
diff --git a/src/ThePathfinder.ts b/src/ThePathfinder.ts
index 173337f..755d0e7 100644
--- a/src/ThePathfinder.ts
+++ b/src/ThePathfinder.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires, @typescript-eslint/consistent-type-assertions, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/restrict-template-expressions, no-unmodified-loop-condition */
import { Bot, BotEvents } from 'mineflayer'
import { AStarBackOff as AAStar } from './abstract/algorithms/astar'
import { AStar, Path, PathProducer } from './mineflayer-specific/algs'
@@ -10,12 +11,12 @@ import type {
BuildableMoveExecutor,
BuildableMoveProvider,
MovementOptions,
- ExecutorMap,
+ ExecutorMap
} from './mineflayer-specific/movements'
import {
MovementHandler,
MovementExecutor,
- DEFAULT_MOVEMENT_OPTS
+ buildMovementOptions
} from './mineflayer-specific/movements'
import {
@@ -126,15 +127,15 @@ export class ThePathfinder {
optimizers: OptimizationMap
}
- public get currentAStar(): AStar | undefined {
+ public get currentAStar (): AStar | undefined {
return this._currentProducer?.getAstarContext()
}
- public get currentProducer(): PathProducer | undefined {
+ public get currentProducer (): PathProducer | undefined {
return this._currentProducer
}
- private snapshotMappings(): {
+ private snapshotMappings (): {
movements: ExecutorMap
optimizers: OptimizationMap
} {
@@ -144,72 +145,71 @@ export class ThePathfinder {
}
}
- private get activeMappings(): {
+ private get activeMappings (): {
movements: ExecutorMap
optimizers: OptimizationMap
} {
return this._gotoMappings ?? this.snapshotMappings()
}
- public getActiveMappings(): ExecutionMappings {
+ public getActiveMappings (): ExecutionMappings {
return this.activeMappings
}
- public get isPathing(): boolean {
+ public get isPathing (): boolean {
return !this.executeTask.done
}
- public get currentGoal(): Readonly | undefined {
- return this.currentGotoGoal;
+ public get currentGoal (): Readonly | undefined {
+ return this.currentGotoGoal
}
- public get resetReason(): ResetReason | undefined {
+ public get resetReason (): ResetReason | undefined {
return this.executionRunner.getResetReason()
}
- public get currentIndex(): number {
- return this.executionRunner.getCurrentIndex();
+ public get currentIndex (): number {
+ return this.executionRunner.getCurrentIndex()
}
- public get currentPath(): Move[] | undefined {
+ public get currentPath (): Move[] | undefined {
return this.executionRunner.getCurrentPath()
}
- public set currentPath(path: Move[] | undefined) {
+ public set currentPath (path: Move[] | undefined) {
this.executionRunner.setCurrentPath(path)
}
- public get currentMove(): Move | undefined {
+ public get currentMove (): Move | undefined {
return this.executionRunner.getCurrentMove()
}
- public set currentMove(move: Move | undefined) {
+ public set currentMove (move: Move | undefined) {
this.executionRunner.setCurrentMove(move)
}
- public get currentExecutor(): MovementExecutor | undefined {
+ public get currentExecutor (): MovementExecutor | undefined {
return this.executionRunner.getCurrentExecutor()
}
- public set currentExecutor(executor: MovementExecutor | undefined) {
+ public set currentExecutor (executor: MovementExecutor | undefined) {
this.executionRunner.setCurrentExecutor(executor)
}
-
- public clearResetReason(): void {
+ public clearResetReason (): void {
this.executionRunner.clearResetReason()
}
reconstructPath = reconstructPath
- constructor(private readonly bot: Bot, opts: HandlerOpts = {}) {
+ constructor (private readonly bot: Bot, opts: HandlerOpts = {}) {
this.world = opts.world ?? new CacheSyncWorld(bot, bot.world)
const moveSettings: MovementOptions = {} as MovementOptions
const pathfinderSettings: PathfinderOptions = {} as PathfinderOptions
const optimizers = opts.optimizers ?? DEFAULT_OPTIMIZATION
const moveSetup = opts.movements ?? DEFAULT_SETUP
- Object.assign(moveSettings, { ...DEFAULT_MOVEMENT_OPTS, ...opts.moveSettings })
+ Object.assign(moveSettings, buildMovementOptions(opts.moveSettings))
Object.assign(pathfinderSettings, { ...DEFAULT_PATHFINDER_OPTS, ...opts.pathfinderSettings })
const moves = new Map()
@@ -231,7 +231,7 @@ export class ThePathfinder {
log('Pathfinder initialized.')
}
- setExecutor(provider: BuildableMoveProvider, Executor: BuildableMoveExecutor | MovementExecutor): void {
+ setExecutor (provider: BuildableMoveProvider, Executor: BuildableMoveExecutor | MovementExecutor): void {
if (Executor instanceof MovementExecutor) {
this.movements.set(provider, Executor)
} else {
@@ -239,8 +239,7 @@ export class ThePathfinder {
}
}
-
- setOptimizer(
+ setOptimizer (
provider: BuildableMoveProvider,
Optimizer: BuildableMoveOptimizer | MovementOptimizer,
Executor?: BuildableMoveExecutor | MovementExecutor,
@@ -259,7 +258,7 @@ export class ThePathfinder {
this.optimizerRegistry.setOptimizer(provider, optimizer, optimizedExecutor, priority)
}
- addOptimizer(
+ addOptimizer (
provider: BuildableMoveProvider,
Optimizer: BuildableMoveOptimizer | MovementOptimizer,
Executor?: BuildableMoveExecutor | MovementExecutor,
@@ -278,53 +277,53 @@ export class ThePathfinder {
this.optimizerRegistry.addOptimizer(provider, optimizer, optimizedExecutor, priority)
}
- setMoveOptions(settings: Partial): void {
- this.defaultMoveSettings = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings)
+ setMoveOptions (settings: Partial): void {
+ this.defaultMoveSettings = buildMovementOptions(settings)
this.optimizerRegistry.setSettings(this.defaultMoveSettings)
for (const [, executor] of this.movements) {
executor.settings = this.defaultMoveSettings
}
}
- setOptions(settings: Partial): void {
+ setOptions (settings: Partial): void {
this.pathfinderSettings = Object.assign({}, DEFAULT_PATHFINDER_OPTS, settings)
}
- dropMovment(provider: BuildableMoveProvider): void {
+ dropMovment (provider: BuildableMoveProvider): void {
this.movements.delete(provider)
}
- dropAllMovements(): void {
+ dropAllMovements (): void {
this.movements.clear()
}
- async cancel(): Promise {
+ async cancel (): Promise {
log('User cancelled pathfinding.')
await this.interrupt(this.defaultMoveSettings.movementTimeoutMs, true, 'goalReassignment')
}
- async interrupt(timeout = this.defaultMoveSettings.movementTimeoutMs, cancelCalculation = true, reasonStr?: ResetReason): Promise {
+ async interrupt (timeout = this.defaultMoveSettings.movementTimeoutMs, cancelCalculation = true, reasonStr?: ResetReason): Promise {
log('Interrupt called. Cancel Calculation: %s. %s', cancelCalculation, reasonStr)
if (this._currentProducer == null) return log('Interrupt ignored: no producer')
this.abortCalculation = cancelCalculation
- const currentExecutor = this.currentExecutor;
- const currentMove = this.currentMove;
+ const currentExecutor = this.currentExecutor
+ const currentMove = this.currentMove
if (currentExecutor == null) return log('Interrupt ignored: no executor')
if (currentMove == null) throw new Error('No current move, but there is a current executor.')
- const reason = reasonStr ? ResetError.fromReason(reasonStr) : undefined;
- this.executionRunner.setResetReason(reasonStr);
+ const reason = reasonStr ? ResetError.fromReason(reasonStr) : undefined
+ this.executionRunner.setResetReason(reasonStr)
await currentExecutor.abort(currentMove, { timeout, reason })
}
- async reset(reason: ResetReason, cancelTimeout = this.defaultMoveSettings.movementTimeoutMs): Promise {
+ async reset (reason: ResetReason, cancelTimeout = this.defaultMoveSettings.movementTimeoutMs): Promise {
log('Reset triggered due to: %s', reason)
this.bot.emit('resetPath', reason)
await this.interrupt(cancelTimeout, true, reason)
}
- setupListeners(): void {
+ setupListeners (): void {
const disposeBlockUpdateListener = handleSettledBlockEvent(
this.bot,
async (oldBlock: Block | null, newBlock: Block | null, settledBlock: Block | null) => {
@@ -338,7 +337,7 @@ export class ThePathfinder {
// settledBlock?.position
// )
- const currentPath = this.currentPath;
+ const currentPath = this.currentPath
if (oldBlock == null || settledBlock == null) return
if (currentPath == null) return
if (oldBlock.type === settledBlock.type) return // break in progress.
@@ -370,11 +369,10 @@ export class ThePathfinder {
void this.reset('chunkLoad')
}
})
-
}
- public updateMatchesWanted(block: Block | null, path: Move[] | undefined = this.currentPath): boolean {
- const currentIndex = this.currentIndex;
+ public updateMatchesWanted (block: Block | null, path: Move[] | undefined = this.currentPath): boolean {
+ const currentIndex = this.currentIndex
log(`block: ${block?.name} pos: ${block?.position}, path: ${path?.length}, index: ${currentIndex}`)
if (block == null || path == null) return false
@@ -405,7 +403,7 @@ export class ThePathfinder {
return false
}
- isPositionNearPath(pos: Vec3 | undefined, path: Move[] | undefined = this.currentPath): boolean {
+ isPositionNearPath (pos: Vec3 | undefined, path: Move[] | undefined = this.currentPath): boolean {
if (pos == null || path == null) return false
for (let i = this.currentIndex; i < path.length; i++) {
@@ -426,7 +424,7 @@ export class ThePathfinder {
return false
}
- private registerAll(
+ private registerAll (
goal: goals.GoalDynamic,
opts: { onHasUpdate?: () => void, onInvalid?: () => void, onCleanup?: () => void, forAll?: () => void }
): () => void {
@@ -470,7 +468,7 @@ export class ThePathfinder {
for (const key of goal._eventKeys) {
const listener = (...args: Parameters): void => {
- if (this.resetReason === "goalReassignment") return cleanup()
+ if (this.resetReason === 'goalReassignment') return cleanup()
if (boundEvent(key, ...args)) newOnHasUpdate()
}
this.bot.on(key, listener)
@@ -479,7 +477,7 @@ export class ThePathfinder {
for (const key of goal._validKeys) {
const listener1 = (...args: Parameters): void => {
- if ((this.resetReason === "goalReassignment")) return cleanup()
+ if ((this.resetReason === 'goalReassignment')) return cleanup()
if (boundValid(key, ...args)) newOnInvalid()
}
this.bot.on(key, listener1)
@@ -491,12 +489,12 @@ export class ThePathfinder {
return cleanup
}
- getPathTo(goal: goals.Goal, settings = this.defaultMoveSettings): PathGenerator {
+ getPathTo (goal: goals.Goal, settings = this.defaultMoveSettings): PathGenerator {
const { movements } = this.activeMappings
return this.getPathFromTo(this.bot.entity.position, this.bot.entity.velocity, goal, settings, movements)
}
- async * getPathFromTo(
+ async * getPathFromTo (
startPos: Vec3,
startVel: Vec3,
goal: goals.Goal,
@@ -509,7 +507,6 @@ export class ThePathfinder {
startPos = getSupportedStartPos(this.world, getNormalizedPos(this.bot, startPos))
log('Generating path from %O to %O', startPos, goal)
-
const startMove = Move.startMove(
new IdleMovement(this.bot, this.world),
startPos.clone(),
@@ -594,7 +591,7 @@ export class ThePathfinder {
}
}
- async getPathFromToRaw(startPos: Vec3, startVel: Vec3, goal: goals.Goal): Promise {
+ async getPathFromToRaw (startPos: Vec3, startVel: Vec3, goal: goals.Goal): Promise {
for await (const res of this.getPathFromTo(startPos, startVel, goal)) {
if (res.result.status !== 'success') {
if (res.result.status === 'noPath' || res.result.status === 'timeout') return null
@@ -605,7 +602,7 @@ export class ThePathfinder {
return null
}
- async goto(goal: goals.Goal, performOpts: PerformOpts = {}): Promise {
+ async goto (goal: goals.Goal, performOpts: PerformOpts = {}): Promise {
log('goto called')
if (goal == null) {
await this.cancel()
@@ -640,7 +637,7 @@ export class ThePathfinder {
}
}
- private async _goto(goal: goals.Goal, performOpts: PerformOpts = {}): Promise {
+ private async _goto (goal: goals.Goal, performOpts: PerformOpts = {}): Promise {
const doForever = !!(goal instanceof goals.GoalDynamic && goal.neverfinish && goal.dynamic)
let toWaitOn = Promise.resolve()
@@ -689,7 +686,7 @@ export class ThePathfinder {
res1 = null
})
} else {
- // Update the unoptimized path in place!
+ // Update the unoptimized path in place!
// perform() will automatically catch the newly added tail.
res1.path.length = res.result.path.length
for (let i = 0; i < res.result.path.length; i++) {
@@ -736,38 +733,38 @@ export class ThePathfinder {
manualCleanup()
}
}
- } while (this.resetReason !== "goalReassignment" && madeIt === false)
+ } while (this.resetReason !== 'goalReassignment' && !madeIt)
await this.cleanupBot()
if (doForever) {
- if (this.resetReason == null && this.resetReason !== "goalReassignment") {
+ if (this.resetReason == null && this.resetReason !== 'goalReassignment') {
await toWaitOn
}
}
- } while (doForever && this.resetReason !== "goalReassignment")
+ } while (doForever && this.resetReason !== 'goalReassignment')
}
- perform(path: Path, goal: goals.Goal, entry = 0): Promise {
- return this.executionRunner.perform(path, goal, entry)
+ async perform (path: Path, goal: goals.Goal, entry = 0): Promise {
+ return await this.executionRunner.perform(path, goal, entry)
}
- recovery(move: Move, path: Path, goal: goals.Goal, entry = 0): Promise {
- return this.executionRunner.recovery(move, path, goal, entry)
+ async recovery (move: Move, path: Path, goal: goals.Goal, entry = 0): Promise {
+ return await this.executionRunner.recovery(move, path, goal, entry)
}
- async cleanupBot(forceSafety = false): Promise {
+ async cleanupBot (forceSafety = false): Promise {
this.bot.clearControlStates()
// rough code. just need any of them.
- let exec;
+ let exec
for (const [, executor] of this.activeMappings.movements) {
- exec ??= executor;
+ exec ??= executor
executor.reset()
}
if (forceSafety && exec != null) {
- let normVel;
+ let normVel
do {
normVel = this.bot.entity.onGround ? this.bot.entity.velocity.offset(0, -this.bot.entity.velocity.y, 0) : this.bot.entity.velocity
const ectx = exec.simForward({ ticks: 2 })
@@ -784,17 +781,16 @@ export class ThePathfinder {
}
}
- cleanupClient(): void {
+ cleanupClient (): void {
this.abortCalculation = false
this.clearResetReason()
delete this.currentGotoGoal
- delete this.currentPath;
- delete this.currentExecutor;
- delete this.currentMove;
-
+ delete this.currentPath
+ delete this.currentExecutor
+ delete this.currentMove
}
- async cleanupAll(goal: goals.Goal, executor = this.currentExecutor): Promise {
+ async cleanupAll (goal: goals.Goal, executor = this.currentExecutor): Promise {
if (goal instanceof goals.GoalDynamic && goal.dynamic) {
goal.cleanup?.()
}
@@ -806,7 +802,7 @@ export class ThePathfinder {
}
this.world.cleanup?.()
- if ((this.resetReason === "goalReassignment")) {
+ if ((this.resetReason === 'goalReassignment')) {
log('Cleanup: Goal aborted.')
this.bot.emit('goalAborted', goal)
} else {
@@ -817,7 +813,7 @@ export class ThePathfinder {
this.abortCalculation = false
this.executeTask.finish()
- log(`Task finished, cleanup client.`)
+ log('Task finished, cleanup client.')
this.cleanupClient()
}
}
diff --git a/src/abstract/algorithms/astar.ts b/src/abstract/algorithms/astar.ts
index cc36bb5..e2d617f 100644
--- a/src/abstract/algorithms/astar.ts
+++ b/src/abstract/algorithms/astar.ts
@@ -20,7 +20,7 @@ export class AStar
maxCost: number
- checkInterval = 4// 1 << 5 - 1
+ checkInterval = 4// 1 << 5 - 1
nodeConsiderCount = 0
constructor (
diff --git a/src/customBlockEvents.ts b/src/customBlockEvents.ts
index 7dc4df6..34cd4c8 100644
--- a/src/customBlockEvents.ts
+++ b/src/customBlockEvents.ts
@@ -1,14 +1,14 @@
+/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/explicit-function-return-type, @typescript-eslint/restrict-template-expressions */
// blockEvents.ts
-const createDebug = require('debug')
import type { Bot } from 'mineflayer'
import type { Block } from 'prismarine-block'
import type { Vec3 } from 'vec3'
-
+const createDebug = require('debug')
export type BlockUpdateListener = (oldBlock: Block | null, newBlock: Block | null) => void
export type BlockPositionString = `(${number}, ${number}, ${number})`
-export type BlockPositionEventName = `blockUpdate:(x, y, z)`
+export type BlockPositionEventName = 'blockUpdate:(x, y, z)'
export type BlockEventName = 'blockUpdate' | BlockPositionEventName
type BlockEventListenerMap = {
@@ -17,17 +17,17 @@ type BlockEventListenerMap = {
[K in BlockPositionEventName]: BlockUpdateListener
}
-const debugIt = false;
+const debugIt = false
const listenerCounts = new WeakMap>()
-function debug(...args: any[]) {
+function debug (...args: any[]) {
const log = createDebug('minecraft-pathfinding:block-events')
if (debugIt) {
log(...args)
}
}
-function getTrackedListenerCounts(bot: Bot): Map {
+function getTrackedListenerCounts (bot: Bot): Map {
let counts = listenerCounts.get(bot)
if (counts == null) {
counts = new Map()
@@ -36,18 +36,18 @@ function getTrackedListenerCounts(bot: Bot): Map {
return counts
}
-function changeTrackedListenerCount(bot: Bot, eventName: BlockEventName, delta: number): number {
+function changeTrackedListenerCount (bot: Bot, eventName: BlockEventName, delta: number): number {
const counts = getTrackedListenerCounts(bot)
const next = Math.max(0, (counts.get(eventName) ?? 0) + delta)
counts.set(eventName, next)
return next
}
-export function getBlockEventListenerCount(bot: Bot, eventName: BlockEventName): number {
+export function getBlockEventListenerCount (bot: Bot, eventName: BlockEventName): number {
return getTrackedListenerCounts(bot).get(eventName) ?? 0
}
-export function getTotalBlockEventListenerCount(bot: Bot): number {
+export function getTotalBlockEventListenerCount (bot: Bot): number {
let total = 0
for (const count of getTrackedListenerCounts(bot).values()) {
total += count
@@ -55,11 +55,11 @@ export function getTotalBlockEventListenerCount(bot: Bot): number {
return total
}
-export function toBlockPositionEventName(position: Vec3): BlockPositionEventName {
+export function toBlockPositionEventName (position: Vec3): BlockPositionEventName {
return `blockUpdate:${position.toString()}` as BlockPositionEventName
}
-export function onBlockEvent(
+export function onBlockEvent (
bot: Bot,
eventName: K,
listener: BlockEventListenerMap[K]
@@ -75,7 +75,7 @@ export function onBlockEvent(
}
}
-export function onceBlockEvent(
+export function onceBlockEvent (
bot: Bot,
eventName: K,
listener: BlockEventListenerMap[K]
@@ -97,7 +97,7 @@ export function onceBlockEvent(
debug('active listeners %s=%d total=%d', eventName, active, getTotalBlockEventListenerCount(bot))
}
-export function offBlockEvent(
+export function offBlockEvent (
bot: Bot,
eventName: K,
listener: BlockEventListenerMap[K]
@@ -108,7 +108,7 @@ export function offBlockEvent(
debug('active listeners %s=%d total=%d', eventName, active, getTotalBlockEventListenerCount(bot))
}
-export function handleBlockEvent(
+export function handleBlockEvent (
bot: Bot,
eventName: K,
listener: BlockEventListenerMap[K]
@@ -117,7 +117,7 @@ export function handleBlockEvent(
return () => offBlockEvent(bot, eventName, listener)
}
-export function handleBlockPositionEvent(
+export function handleBlockPositionEvent (
bot: Bot,
position: Vec3,
listener: BlockUpdateListener
@@ -130,19 +130,19 @@ export interface WaitForBlockEventOptions {
signal?: AbortSignal
}
-export function waitForBlockEvent(
+export async function waitForBlockEvent (
bot: Bot,
eventName: K,
opts: WaitForBlockEventOptions = {}
): Promise> {
const { timeoutMs = 5000, signal } = opts
- return new Promise((resolve, reject) => {
+ return await new Promise((resolve, reject) => {
let finished = false
let timeout: NodeJS.Timeout | undefined
const cleanup = () => {
- if (timeout) clearTimeout(timeout)
+ if (timeout != null) clearTimeout(timeout)
offBlockEvent(bot, eventName, listener)
signal?.removeEventListener('abort', onAbort)
}
@@ -179,7 +179,7 @@ export function waitForBlockEvent(
}, timeoutMs)
}
- if (signal) {
+ if (signal != null) {
if (signal.aborted) {
fail(new Error(`Aborted while waiting for ${eventName}`))
return
@@ -213,7 +213,7 @@ export type SettledBlockUpdateListener = (
settledBlock: Block | null
) => void | Promise
-type PositionSettleController = {
+interface PositionSettleController {
resolve: (value: TResult) => void
reject: (err: Error) => void
finishGuard: () => boolean
@@ -223,7 +223,7 @@ type PositionSettleController = {
bot: Bot
}
-function waitForPositionSettle(
+async function waitForPositionSettle (
bot: Bot,
position: Vec3,
opts: {
@@ -245,7 +245,7 @@ function waitForPositionSettle(
const settleMs = opts.settleMs ?? 75
const signal = opts.signal
- return new Promise((resolve, reject) => {
+ return await new Promise((resolve, reject) => {
let finished = false
let timeoutTimer: NodeJS.Timeout | undefined
let settleTimer: NodeJS.Timeout | undefined
@@ -267,7 +267,7 @@ function waitForPositionSettle(
}
const armSettleTimer = (fn: () => void) => {
- if (settleTimer) clearTimeout(settleTimer)
+ if (settleTimer != null) clearTimeout(settleTimer)
settleTimer = setTimeout(() => {
if (finished) return
fn()
@@ -275,7 +275,7 @@ function waitForPositionSettle(
}
const cancelSettleTimer = () => {
- if (settleTimer) {
+ if (settleTimer != null) {
clearTimeout(settleTimer)
settleTimer = undefined
}
@@ -310,12 +310,12 @@ function waitForPositionSettle(
fail(new Error(`Aborted while waiting for ${eventName}`))
}
- const cleanup = () => {
- if (timeoutTimer) clearTimeout(timeoutTimer)
- cancelSettleTimer()
- offBlockEvent(bot, 'blockUpdate', onUpdate)
- signal?.removeEventListener('abort', onAbort)
- }
+ const cleanup = () => {
+ if (timeoutTimer != null) clearTimeout(timeoutTimer)
+ cancelSettleTimer()
+ offBlockEvent(bot, 'blockUpdate', onUpdate)
+ signal?.removeEventListener('abort', onAbort)
+ }
onBlockEvent(bot, 'blockUpdate', onUpdate)
@@ -329,7 +329,7 @@ function waitForPositionSettle(
}, timeoutMs)
}
- if (signal) {
+ if (signal != null) {
if (signal.aborted) {
fail(new Error(`Aborted while waiting for ${eventName}`))
return
@@ -338,7 +338,7 @@ function waitForPositionSettle(
}
})
}
-export async function waitForSettledBlockPredicate(
+export async function waitForSettledBlockPredicate (
bot: Bot,
position: Vec3,
predicate: (block: Block | null) => boolean,
@@ -354,7 +354,7 @@ export async function waitForSettledBlockPredicate(
const cancel = () => {
const settleTimer = (controller as any).settleTimer as NodeJS.Timeout | undefined
- if (settleTimer) {
+ if (settleTimer != null) {
clearTimeout(settleTimer)
;(controller as any).settleTimer = undefined
}
@@ -390,7 +390,7 @@ export async function waitForSettledBlockPredicate(
})
}
-export async function waitForSettledBlockStateAtPosition(
+export async function waitForSettledBlockStateAtPosition (
bot: Bot,
position: Vec3,
predicate: (block: Block | null) => boolean,
@@ -404,7 +404,7 @@ export async function waitForSettledBlockStateAtPosition(
createListener: (controller) => {
const cancel = () => {
const settleTimer = (controller as any).settleTimer as NodeJS.Timeout | undefined
- if (settleTimer) {
+ if (settleTimer != null) {
clearTimeout(settleTimer)
;(controller as any).settleTimer = undefined
}
@@ -445,7 +445,7 @@ export async function waitForSettledBlockStateAtPosition(
})
}
-export async function waitForSettledBlockUpdateAtPosition(
+export async function waitForSettledBlockUpdateAtPosition (
bot: Bot,
position: Vec3,
opts: WaitForSettledBlockUpdateOptions = {}
@@ -478,7 +478,7 @@ export async function waitForSettledBlockUpdateAtPosition(
* - Deduplicates concurrent checks per position
* - Reads the final block from the world before invoking the listener
*/
-export function handleSettledBlockEvent(
+export function handleSettledBlockEvent (
bot: Bot,
listener: SettledBlockUpdateListener,
opts: HandleSettledBlockEventOptions = {}
diff --git a/src/index.ts b/src/index.ts
index 4805c55..aa49363 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { Bot } from 'mineflayer'
import { BlockInfo } from './mineflayer-specific/world/cacheWorld'
import { PathfinderOptions, ThePathfinder } from './ThePathfinder'
@@ -16,7 +17,7 @@ import type { MovementOptions, MovementSetup } from './mineflayer-specific/movem
import { MovementProvider } from './mineflayer-specific/movements'
import type { OptimizationSetup } from './mineflayer-specific/post'
-export function createPlugin(opts?: HandlerOpts) {
+export function createPlugin (opts?: HandlerOpts) {
return function (bot: Bot) {
BlockInfo.init(bot.registry) // set up block info
if (!bot.hasPlugin(utilPlugin)) bot.loadPlugin(utilPlugin)
@@ -50,8 +51,14 @@ export * as goals from './mineflayer-specific/goals'
export { MovementExecutor, MovementProvider } from './mineflayer-specific/movements'
export type { BuildableMoveExecutor, BuildableMoveProvider, MovementSetup } from './mineflayer-specific/movements'
+export type { MovementOptions } from './mineflayer-specific/movements'
+
+// Exclusion zones ("keep out" areas), like upstream mineflayer-pathfinder.
+// Only the type is exported; users write their own zone functions
+// (see examples/exclusionZones.js for ready-to-copy box/radius helpers).
+export type { ExclusionArea } from './mineflayer-specific/movements/exclusionZones'
export { MovementOptimizer } from './mineflayer-specific/post'
export type { BuildableMoveOptimizer, OptimizationSetup, OptimizationMap } from './mineflayer-specific/post'
export { Move } from './mineflayer-specific/move'
-export * as movementProviders from './mineflayer-specific/movements/movementProviders'
\ No newline at end of file
+export * as movementProviders from './mineflayer-specific/movements/movementProviders'
diff --git a/src/mineflayer-specific/algs.ts b/src/mineflayer-specific/algs.ts
index e6393ec..386370e 100644
--- a/src/mineflayer-specific/algs.ts
+++ b/src/mineflayer-specific/algs.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { Goal, MovementProvider as AMovementProvider, Path as APath } from '../abstract'
import { AStarBackOff as AAStarBackOff } from '../abstract/algorithms/astar'
import { CPathNode } from '../abstract/node'
@@ -11,7 +12,6 @@ export interface Path extends APath ${afterTick}`)
this.name = 'TickAdvanceError'
}
-}
\ No newline at end of file
+}
diff --git a/src/mineflayer-specific/goals.ts b/src/mineflayer-specific/goals.ts
index a39f8e3..5597941 100644
--- a/src/mineflayer-specific/goals.ts
+++ b/src/mineflayer-specific/goals.ts
@@ -282,7 +282,7 @@ export class GoalBlock extends Goal {
const dx = this.x - node.x
const dy = this.y - node.y
const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone.
+ return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone.
// return (Math.sqrt(dx * dx + dz * dz) + Math.abs(dy))
// return distanceXZ(dx, dz) + Math.abs(dy)
}
@@ -291,7 +291,7 @@ export class GoalBlock extends Goal {
const dx = this.x - node.x
const dy = this.y - node.y
const dz = this.z - node.z
- const distance = Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone.
+ const distance = Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone.
return distance
}
@@ -332,7 +332,7 @@ export class GoalNear extends Goal {
const dx = this.x - node.x
const dy = this.y - node.y
const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone.
+ return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone.
}
distHeuristic (node: Move): number {
@@ -363,7 +363,7 @@ export class GoalNearXZ extends Goal {
heuristic (node: Move): number {
const dx = this.x - node.x
const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz) * (20 / 4.317); // from baritone.
+ return Math.sqrt(dx * dx + dz * dz) * (20 / 4.317) // from baritone.
}
distHeuristic (node: Move): number {
@@ -418,15 +418,15 @@ export class GoalLookAt extends Goal {
const dx = this.x - node.x
const dy = this.y - (node.y + this.eyeHeight) // eye level
const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317); // from baritone.
+ return Math.sqrt(dx * dx + dz * dz + dy * dy) * (20 / 4.317) // from baritone.
}
distHeuristic (node: Move): number {
- const dx = this.x - node.x
- const dy = this.y - (node.y + this.eyeHeight) // eye level
- const dz = this.z - node.z
- const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)
- return distance
+ const dx = this.x - node.x
+ const dy = this.y - (node.y + this.eyeHeight) // eye level
+ const dz = this.z - node.z
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)
+ return distance
}
/**
@@ -559,10 +559,9 @@ export class GoalFollowEntity extends GoalDynamic<'entityMoved', 'entityGone'> {
const dy = this.y - node.y
const dz = this.z - node.z
- return Math.sqrt(dx * dx + dy * dy + dz * dz) * (20 / 4.317); // from baritone.
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) * (20 / 4.317) // from baritone.
}
-
distHeuristic (node: Move): number {
const dx = this.x - node.x
const dy = this.y - node.y
diff --git a/src/mineflayer-specific/movements/controls.ts b/src/mineflayer-specific/movements/controls.ts
index 3298ce5..d178255 100644
--- a/src/mineflayer-specific/movements/controls.ts
+++ b/src/mineflayer-specific/movements/controls.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/consistent-type-assertions */
import { IEntityState } from '@nxg-org/mineflayer-physics-util'
import { Bot } from 'mineflayer'
import { Vec3 } from 'vec3'
@@ -237,7 +238,7 @@ export function botSmartMovement (bot: Bot, nextPoint: Vec3, sprint: boolean, mi
smartMovement(stateLike, nextPoint, sprint, minDist)
}
-type StrafeState = {
+interface StrafeState {
pos: Vec3
vel: Vec3
yaw: number
diff --git a/src/mineflayer-specific/movements/exclusionZones.ts b/src/mineflayer-specific/movements/exclusionZones.ts
new file mode 100644
index 0000000..13170e7
--- /dev/null
+++ b/src/mineflayer-specific/movements/exclusionZones.ts
@@ -0,0 +1,16 @@
+import type { BlockInfo } from '../world/cacheWorld'
+
+/**
+ * An exclusion area: a function that returns the extra cost of letting the bot
+ * use a given block.
+ *
+ * - `0` -> no opinion on this block.
+ * - a positive number -> a soft penalty; the bot avoids the block when it can.
+ * - `>= COST_INF` -> a hard "keep out"; the bot will never use the block.
+ *
+ * These are stored in the three movement settings `exclusionAreasStep`,
+ * `exclusionAreasBreak` and `exclusionAreasPlace`. The pathfinder intentionally
+ * ships no ready-made shapes — write your own, or copy the box/radius helpers
+ * from `examples/exclusionZones.js`. This mirrors upstream mineflayer-pathfinder.
+ */
+export type ExclusionArea = (block: BlockInfo) => number
diff --git a/src/mineflayer-specific/movements/index.ts b/src/mineflayer-specific/movements/index.ts
index e62d625..c03624b 100644
--- a/src/mineflayer-specific/movements/index.ts
+++ b/src/mineflayer-specific/movements/index.ts
@@ -16,4 +16,5 @@ export * from './movementExecutors'
export * from './movementProviders'
export * from './movementExecutor'
export * from './movementProvider'
+export * from './exclusionZones'
// export * from './pp'
diff --git a/src/mineflayer-specific/movements/interactionUtils.ts b/src/mineflayer-specific/movements/interactionUtils.ts
index 311e592..3435e7d 100644
--- a/src/mineflayer-specific/movements/interactionUtils.ts
+++ b/src/mineflayer-specific/movements/interactionUtils.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions, @typescript-eslint/strict-boolean-expressions */
import { Bot, BotEvents } from 'mineflayer'
import { Vec3 } from 'vec3'
@@ -21,7 +22,6 @@ const logBreak = debug('minecraft-pathfinding:BreakHandler')
export type InteractType = 'water' | 'solid' | 'replaceable'
-
interface InteractionPerformInfo {
ticks: number
tickAllowance: number
@@ -55,23 +55,23 @@ export abstract class InteractHandler {
protected readonly move!: MovementExecutor
- protected get settings(): MovementOptions {
+ protected get settings (): MovementOptions {
return this.move.settings
}
- public get vec(): Vec3 {
+ public get vec (): Vec3 {
return new Vec3(this.x, this.y, this.z)
}
- public get bb(): AABB {
+ public get bb (): AABB {
return AABB.fromBlock(this.vec)
}
- public get equipping(): boolean {
+ public get equipping (): boolean {
return this._equipping
}
- constructor(
+ constructor (
public readonly x: number,
public readonly y: number,
public readonly z: number,
@@ -81,32 +81,32 @@ export abstract class InteractHandler {
this.blockInfo = this.toBlockInfo()
}
- public get isPerforming(): boolean {
+ public get isPerforming (): boolean {
return this.performing
}
- public get done(): boolean {
+ public get done (): boolean {
return this._done
}
- public get allowExit(): boolean {
+ public get allowExit (): boolean {
return !this._internalLock
}
- public loadMove(move: MovementExecutor): void {
+ public loadMove (move: MovementExecutor): void {
(this as any).move = move
}
- abstract needToPerform(bot: Bot): boolean
+ abstract needToPerform (bot: Bot): boolean
- abstract getItem(bot: Bot, block?: Block): Item | null
- abstract perform(bot: Bot, item: Item | null, opts?: InteractOpts): Promise
- abstract performInfo(bot: Bot, ticks?: number): Promise
- abstract toBlockInfo(): BlockInfo
+ abstract getItem (bot: Bot, block?: Block): Item | null
+ abstract perform (bot: Bot, item: Item | null, opts?: InteractOpts): Promise
+ abstract performInfo (bot: Bot, ticks?: number): Promise
+ abstract toBlockInfo (): BlockInfo
- abstract abort(bot: Bot): Promise
+ abstract abort (bot: Bot): Promise
- public async _abort(bot: Bot): Promise {
+ public async _abort (bot: Bot): Promise {
if (this.performing && !this.cancelled) {
logBase(`Aborting interaction at ${this.vec}`)
await this.abort(bot)
@@ -115,7 +115,7 @@ export abstract class InteractHandler {
}
}
- public async _perform(bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise {
+ public async _perform (bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise {
if (this.performing) {
logBase(`Error: Already performing interaction at ${this.vec}`)
throw new Error('Already performing')
@@ -143,14 +143,14 @@ export abstract class InteractHandler {
return ret
}
- getCurrentItem(bot: Bot): Item | null {
+ getCurrentItem (bot: Bot): Item | null {
if (this.offhand) return bot.inventory.slots[bot.getEquipmentDestSlot('off-hand')]
return bot.inventory.slots[bot.getEquipmentDestSlot('hand')]
}
- async equipItem(bot: Bot, item: Item | null): Promise {
+ async equipItem (bot: Bot, item: Item | null): Promise {
if (this._equipping) return // already equipping item, ignore silently.
- this._equipping = true;
+ this._equipping = true
if (item === null) {
logBase(`Unequipping ${this.offhand ? 'off-hand' : 'hand'}`)
await bot.unequip(this.offhand ? 'off-hand' : 'hand')
@@ -163,10 +163,10 @@ export abstract class InteractHandler {
}
bot.updateHeldItem()
// await bot.waitForTicks(2)
- this._equipping = false;
+ this._equipping = false
}
- async allowExternalInfluence(bot: Bot, ticks = 1, sneak = false): Promise {
+ async allowExternalInfluence (bot: Bot, ticks = 1, sneak = false): Promise {
if (!this.performing) return true
if (!this._internalLock) return true
@@ -192,16 +192,16 @@ export class PlaceHandler extends InteractHandler {
static reach = 4
private _placeTask?: Promise
- static fromVec(vec: Vec3, type: InteractType, offhand = false): PlaceHandler {
+ static fromVec (vec: Vec3, type: InteractType, offhand = false): PlaceHandler {
return new PlaceHandler(vec.x, vec.y, vec.z, type, offhand)
}
- static identTypeFromItem(item: Item): InteractType {
+ static identTypeFromItem (item: Item): InteractType {
if (item.name.includes('water')) return 'water'
return 'solid'
}
- toBlockInfo(): BlockInfo {
+ toBlockInfo (): BlockInfo {
switch (this.type) {
case 'solid':
return BlockInfo.SOLID(this.vec)
@@ -214,7 +214,7 @@ export class PlaceHandler extends InteractHandler {
}
}
- getItem(bot: Bot): Item | null {
+ getItem (bot: Bot): Item | null {
switch (this.type) {
case 'water': {
return bot.inventory.items().find((item) => item.name === 'water_bucket') ?? null
@@ -230,7 +230,7 @@ export class PlaceHandler extends InteractHandler {
}
}
- getNearbyBlocks(world: World): BlockInfo[] {
+ getNearbyBlocks (world: World): BlockInfo[] {
return [
world.getBlockInfo(this.vec.offset(0, 1, 0)),
world.getBlockInfo(this.vec.offset(0, -1, 0)),
@@ -241,7 +241,7 @@ export class PlaceHandler extends InteractHandler {
]
}
- needToPerform(bot: Bot): boolean {
+ needToPerform (bot: Bot): boolean {
const blockInfo = bot.pathfinder.world.getBlockInfo(this.vec)
if (blockInfo.isInvalid) {
logPlace(`Block at ${this.vec} is invalid. needsToPerform: true`)
@@ -268,7 +268,7 @@ export class PlaceHandler extends InteractHandler {
return needs
}
- async performInfo(bot: Bot, ticks = 15, scale = 0.5): Promise {
+ async performInfo (bot: Bot, ticks = 15, scale = 0.5): Promise {
switch (this.type) {
case 'water': {
throw new Error('Not implemented')
@@ -358,7 +358,7 @@ export class PlaceHandler extends InteractHandler {
}
}
- async perform(bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise {
+ async perform (bot: Bot, item: Item | null, opts: InteractOpts = {}): Promise {
const curInfo = { yaw: bot.entity.yaw, pitch: bot.entity.pitch }
if (item === null) {
@@ -376,7 +376,7 @@ export class PlaceHandler extends InteractHandler {
logPlace(`Looking at ${this.vec} to place water.`)
await bot.lookAt(this.vec, this.settings.forceLook)
bot.activateItem(this.offhand)
- logPlace(`Water placed.`)
+ logPlace('Water placed.')
break
}
@@ -427,10 +427,9 @@ export class PlaceHandler extends InteractHandler {
PlaceHandler.reach * 2
)) as unknown as RayType
-
if (testCheck === null) {
logPlace('what the fuck?')
- break;
+ break
}
const pos1 = testCheck.position.plus(faceToVec(testCheck.face))
@@ -475,10 +474,10 @@ export class PlaceHandler extends InteractHandler {
this._internalLock = false
if (opts.noAwait) {
- this._placeTask.catch((err) => logPlace(`Background place task failed: %O`, err))
+ this._placeTask.catch((err) => logPlace('Background place task failed: %O', err))
} else {
await this._placeTask
- logPlace(`_placeTask resolved.`)
+ logPlace('_placeTask resolved.')
}
this.task?.finish()
@@ -502,7 +501,7 @@ export class PlaceHandler extends InteractHandler {
logPlace(`Completed perform sequence at ${this.vec}`)
}
- async abort(bot: Bot): Promise {
+ async abort (bot: Bot): Promise {
logPlace(`Aborting placement at ${this.vec}`)
if ((this.task != null) && !this.task.done) {
this.task.finish()
@@ -511,7 +510,7 @@ export class PlaceHandler extends InteractHandler {
if (this._placeTask != null) {
await this._placeTask.catch((err) => {
- logPlace(`Caught error during _placeTask abort: %O`, err)
+ logPlace('Caught error during _placeTask abort: %O', err)
})
}
}
@@ -521,19 +520,19 @@ export class BreakHandler extends InteractHandler {
static reach = 4
private _breakTask?: Promise
- static fromVec(vec: Vec3, type: InteractType, offhand = false): BreakHandler {
+ static fromVec (vec: Vec3, type: InteractType, offhand = false): BreakHandler {
return new BreakHandler(vec.x, vec.y, vec.z, type, offhand)
}
- toBlockInfo(): BlockInfo {
+ toBlockInfo (): BlockInfo {
return BlockInfo.AIR(this.vec)
}
- getBlock(world: World): Block | null {
+ getBlock (world: World): Block | null {
return world.getBlock(this.vec)
}
- getItem(bot: Bot, block: Block): Item | null {
+ getItem (bot: Bot, block: Block): Item | null {
switch (this.type) {
case 'water': {
return bot.inventory.items().find((item) => item.name === 'bucket') ?? null // empty bucket
@@ -550,7 +549,7 @@ export class BreakHandler extends InteractHandler {
}
}
- needToPerform(bot: Bot): boolean {
+ needToPerform (bot: Bot): boolean {
const blockInfo = bot.pathfinder.world.getBlockInfo(this.vec)
if (blockInfo.isInvalid) {
@@ -563,7 +562,7 @@ export class BreakHandler extends InteractHandler {
return needs
}
- async performInfo(bot: Bot, ticks = 15): Promise {
+ async performInfo (bot: Bot, ticks = 15): Promise {
const bb = AABB.fromBlock(this.vec)
const dist = bb.distanceToVec(bot.entity.position.offset(0, 1.62, 0))
const reachable = dist < BreakHandler.reach + 5
@@ -575,7 +574,7 @@ export class BreakHandler extends InteractHandler {
: { ticks: Infinity, tickAllowance: Infinity, shiftTick: Infinity, raycasts: [] }
}
- async perform(bot: Bot, item: Item | null = null, opts: InteractOpts = {}): Promise {
+ async perform (bot: Bot, item: Item | null = null, opts: InteractOpts = {}): Promise {
const curInfo = { yaw: bot.entity.yaw, pitch: bot.entity.pitch }
logBreak(`Starting break sequence at ${this.vec}`)
@@ -588,7 +587,7 @@ export class BreakHandler extends InteractHandler {
logBreak(`Looking at ${this.vec} to collect water.`)
await bot.lookAt(this.vec, this.settings.forceLook)
bot.activateItem(this.offhand)
- logBreak(`Water collected.`)
+ logBreak('Water collected.')
break
}
@@ -611,8 +610,7 @@ export class BreakHandler extends InteractHandler {
logBreak(`Calling bot.dig on ${block.name}`)
this._breakTask = bot.dig(block, 'ignore', 'raycast')
-
- logBreak(`Dig task resolved. Now waiting for world update.`)
+ logBreak('Dig task resolved. Now waiting for world update.')
await waitForSettledBlockStateAtPosition(
bot,
@@ -647,7 +645,7 @@ export class BreakHandler extends InteractHandler {
logBreak(`Completed break sequence at ${this.vec}`)
}
- async abort(bot: Bot): Promise {
+ async abort (bot: Bot): Promise {
logBreak(`Aborting break at ${this.vec}`)
if ((this.task != null) && !this.task.done) {
this.task.finish()
@@ -660,7 +658,7 @@ export class BreakHandler extends InteractHandler {
break
}
case 'solid': {
- logBreak(`Calling bot.stopDigging()`)
+ logBreak('Calling bot.stopDigging()')
bot.stopDigging()
break
}
@@ -669,8 +667,8 @@ export class BreakHandler extends InteractHandler {
}
}
await this._breakTask.catch((err) => {
- logBreak(`Caught error during _breakTask abort: %O`, err)
+ logBreak('Caught error during _breakTask abort: %O', err)
})
}
}
-}
\ No newline at end of file
+}
diff --git a/src/mineflayer-specific/movements/movement.ts b/src/mineflayer-specific/movements/movement.ts
index 40d824f..ff43bd5 100644
--- a/src/mineflayer-specific/movements/movement.ts
+++ b/src/mineflayer-specific/movements/movement.ts
@@ -9,6 +9,7 @@ import type { InteractType } from './interactionUtils'
import type { Block } from '../../types'
import { Vec3Properties } from '../../types'
import { COST_INF } from './costs'
+import type { ExclusionArea } from './exclusionZones'
export interface MovementOptions {
allowDiagonalBridging: boolean
@@ -32,6 +33,25 @@ export interface MovementOptions {
careAboutLookAlignment: boolean
movementTimeoutMs: number
+
+ /**
+ * "Keep out" rules for blocks the bot would STAND in / walk into.
+ *
+ * Each {@link ExclusionArea} is a function that returns the extra cost of a
+ * block (return `>= COST_INF` to forbid it entirely). The cost of every area
+ * in the list is added together. An empty list (the default) means "no zones",
+ * and costs nothing to evaluate.
+ *
+ * Write your own; ready-to-copy box/radius helpers live in
+ * `examples/exclusionZones.js`.
+ */
+ exclusionAreasStep: ExclusionArea[]
+
+ /** "Keep out" rules for blocks the bot would BREAK (mine). See {@link exclusionAreasStep}. */
+ exclusionAreasBreak: ExclusionArea[]
+
+ /** "Keep out" rules for blocks the bot would PLACE (build on). See {@link exclusionAreasStep}. */
+ exclusionAreasPlace: ExclusionArea[]
}
export const DEFAULT_MOVEMENT_OPTS: MovementOptions = {
@@ -53,7 +73,49 @@ export const DEFAULT_MOVEMENT_OPTS: MovementOptions = {
forceLook: true,
careAboutLookAlignment: true,
allowDiagonalBridging: true,
- movementTimeoutMs: 1000
+ movementTimeoutMs: 1000,
+ // No exclusion zones by default. Add your own with bot.pathfinder.setMoveOptions(...).
+ exclusionAreasStep: [],
+ exclusionAreasBreak: [],
+ exclusionAreasPlace: []
+}
+
+// Lock the default exclusion lists so the single shared instances above can never
+// be mutated in place. Real settings always receive their own fresh arrays via
+// buildMovementOptions() below, so this is just a safety net.
+Object.freeze(DEFAULT_MOVEMENT_OPTS.exclusionAreasStep)
+Object.freeze(DEFAULT_MOVEMENT_OPTS.exclusionAreasBreak)
+Object.freeze(DEFAULT_MOVEMENT_OPTS.exclusionAreasPlace)
+
+/**
+ * Merge user-supplied movement options on top of {@link DEFAULT_MOVEMENT_OPTS}
+ * and return a complete {@link MovementOptions}.
+ *
+ * The three exclusion-area lists are ALWAYS returned as their own fresh arrays.
+ * The defaults hold a single shared `[]` per list, so copying here is what stops
+ * two different bots — or two `setMoveOptions` calls — from accidentally sharing
+ * (and then mutating) the same array. Use this everywhere instead of a bare
+ * `Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings)`.
+ */
+export function buildMovementOptions (settings: Partial = {}): MovementOptions {
+ const merged = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings)
+ merged.exclusionAreasStep = [...merged.exclusionAreasStep]
+ merged.exclusionAreasBreak = [...merged.exclusionAreasBreak]
+ merged.exclusionAreasPlace = [...merged.exclusionAreasPlace]
+ return merged
+}
+
+/**
+ * Sum the extra cost every exclusion area in `areas` assigns to `block`.
+ *
+ * Returns 0 immediately when the list is empty (the normal case), so it is
+ * essentially free unless the user opted in to exclusion zones.
+ */
+export function sumExclusionAreas (areas: ExclusionArea[], block: BlockInfo): number {
+ if (areas.length === 0) return 0
+ let weight = 0
+ for (const area of areas) weight += area(block)
+ return weight
}
const cardinalVec3s: Vec3[] = [
@@ -136,7 +198,7 @@ export abstract class Movement {
public constructor (bot: Bot, world: World, settings: Partial = {}) {
this.bot = bot
this.world = world
- this.settings = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings)
+ this.settings = buildMovementOptions(settings)
}
loadMove (move: Move): void {
@@ -182,8 +244,29 @@ export abstract class Movement {
return block.physical ? 0 : COST_INF
}
+ /** Extra cost of STANDING in this block (sum of every step exclusion area; 0 if none). */
+ exclusionStep (block: BlockInfo): number {
+ return sumExclusionAreas(this.settings.exclusionAreasStep, block)
+ }
+
+ /** Extra cost of BREAKING this block (sum of every break exclusion area; 0 if none). */
+ exclusionBreak (block: BlockInfo): number {
+ return sumExclusionAreas(this.settings.exclusionAreasBreak, block)
+ }
+
+ /** Extra cost of PLACING a block here (sum of every place exclusion area; 0 if none). */
+ exclusionPlace (block: BlockInfo): number {
+ return sumExclusionAreas(this.settings.exclusionAreasPlace, block)
+ }
+
/**
- * Takes into account if the block is within a break exclusion area.
+ * Whether this block is allowed to be broken at all (ignoring cost).
+ *
+ * This only answers the "is it physically/configurably breakable" question
+ * (can we dig, would it create flowing water, would a block fall on us, is it
+ * an unbreakable block like bedrock). The "is it inside a no-mining zone"
+ * question is handled separately as a cost, in {@link breakCost} via
+ * {@link exclusionBreak}.
* @param {BlockInfo} block
* @returns
*/
@@ -208,17 +291,23 @@ export abstract class Movement {
}
// console.log('block type:', this.bot.registry.blocks[block.type], block.position, !BlockInfo.blocksCantBreak.has(block.type))
- return BlockInfo.replaceables.has(block.type) || !BlockInfo.blocksCantBreak.has(block.type) // && this.exclusionBreak(block) < COST_INF
+ return BlockInfo.replaceables.has(block.type) || !BlockInfo.blocksCantBreak.has(block.type)
}
/**
- * Takes into account if the block is within the stepExclusionAreas. And returns COST_INF if a block to be broken is within break exclusion areas.
- * @param {import('prismarine-block').Block} block block
- * @param {[]} toBreak
+ * Cost of either walking through `block` (if it is already passable) or
+ * breaking it so the bot can pass.
+ *
+ * Returns `COST_INF` (or more) when the block cannot be used — for example an
+ * unbreakable block, or one inside a break-exclusion zone (see
+ * {@link breakCost} / {@link exclusionBreak}). Step-exclusion zones are NOT
+ * checked here; each movement provider applies {@link exclusionStep} to the
+ * block the bot lands in, folding the cost in before the move is created.
+ * @param {BlockInfo} block block
+ * @param {BreakHandler[]} toBreak
* @returns {number}
*/
safeOrBreak (block: BlockInfo, toBreak: BreakHandler[]): number {
- // cost += this.exclusionStep(block) // Is excluded so can't move or break
// cost += this.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost
// if (block.breakCost !== undefined) return block.breakCost // cache breaking cost.
@@ -256,7 +345,11 @@ export abstract class Movement {
// const effects = this.bot.entity.effects
// const digTime = block.block.digTime(tool ? tool.type : null, false, false, false, enchants, effects)
const laborCost = (1 + 3 * digTime / 1000) * this.settings.digCost
- return laborCost
+
+ // Add the break-exclusion penalty (0 unless the user configured "no mining" zones).
+ // If the block sits inside a forbidden zone this pushes the cost past COST_INF,
+ // which every caller treats as "do not break this block".
+ return laborCost + this.exclusionBreak(block)
}
safeOrPlace (block: BlockInfo, toPlace: PlaceHandler[], type: InteractType = 'solid'): number {
@@ -278,7 +371,9 @@ export abstract class Movement {
* TODO: calculate more accurate place costs.
*/
placeCost (block: BlockInfo): number {
- return this.settings.placeCost
+ // Add the place-exclusion penalty (0 unless the user configured "no building" zones).
+ // A forbidden zone pushes this past COST_INF, which callers treat as "do not place here".
+ return this.settings.placeCost + this.exclusionPlace(block)
}
}
diff --git a/src/mineflayer-specific/movements/movementExecutor.ts b/src/mineflayer-specific/movements/movementExecutor.ts
index 3a4ef9e..42ab3d4 100644
--- a/src/mineflayer-specific/movements/movementExecutor.ts
+++ b/src/mineflayer-specific/movements/movementExecutor.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/member-delimiter-style, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-unused-vars */
import { Bot } from 'mineflayer'
import { Vec3 } from 'vec3'
import { Move } from '../move'
@@ -32,13 +33,13 @@ export interface CompleteOpts {
}
export interface AlignmentBBInfo {
- pos: Vec3;
+ pos: Vec3
requireSupport: boolean
}
export interface InitAlignOpts {
- others?: AlignmentBBInfo[],
- customBB?: AABB;
+ others?: AlignmentBBInfo[]
+ customBB?: AABB
enterExitInterp?: boolean
}
@@ -65,7 +66,7 @@ export abstract class MovementExecutor extends Movement {
/**
* Return the current interaction.
*/
- public get cI(): InteractHandler | undefined {
+ public get cI (): InteractHandler | undefined {
// if (this._cI === undefined) return undefined;
// if (this._cI.allowExit) return undefined;
return this._cI
@@ -83,37 +84,35 @@ export abstract class MovementExecutor extends Movement {
private task: Task = new Task()
- public constructor(bot: Bot, world: World, settings: Partial = {}) {
+ public constructor (bot: Bot, world: World, settings: Partial = {}) {
super(bot, world, settings)
this.engine = new BotcraftPhysics(bot.registry)
this.sim = new BaseSimulator(this.engine)
this.simCtx = EPhysicsCtx.FROM_BOT(this.engine, bot)
}
- public reset(): void {
+ public reset (): void {
// log('Resetting MovementExecutor')
this.aborted = false
delete this.resetReason
this.task.finish()
}
- protected getRemainingBreaks(): BreakHandler[] {
+ protected getRemainingBreaks (): BreakHandler[] {
if (!this.currentMove) return []
return this.currentMove.toBreak.filter(b => b.needToPerform(this.bot))
}
- protected getRemainingPlaces(): PlaceHandler[] {
+ protected getRemainingPlaces (): PlaceHandler[] {
if (!this.currentMove) return []
return this.currentMove.toPlace.filter(b => b.needToPerform(this.bot))
}
- protected isTooLow(thisMove: Move) {
-
- const botY = this.bot.entity.position.y + 0.6; // allow step up
+ protected isTooLow (thisMove: Move) {
+ const botY = this.bot.entity.position.y + 0.6 // allow step up
const block = this.getBlockInfo(this.bot.entity.position, 0, 0, 0)
-
if (botY < thisMove.exitPos.y && botY < thisMove.entryPos.y) {
throw new CancelError(`y level: too low! ${botY}, ${thisMove.entryPos.y} ${thisMove.exitPos.y}`)
}
@@ -122,7 +121,7 @@ export abstract class MovementExecutor extends Movement {
/**
* TODO: Implement.
*/
- public async abort(move: Move = this.currentMove, settings: AbortOpts = {}): Promise {
+ public async abort (move: Move = this.currentMove, settings: AbortOpts = {}): Promise {
// if (this.aborted || this.resetReason != null) return
const resetting = settings.reason
@@ -136,7 +135,7 @@ export abstract class MovementExecutor extends Movement {
this.task = new Task()
}
- private async holdUntilAborted(move: Move, task: Task, timeout = 1000): Promise {
+ private async holdUntilAborted (move: Move, task: Task, timeout = 1000): Promise {
if (!this.aborted && this.resetReason == null) return
log('holdUntilAborted: aborting process started')
@@ -184,7 +183,7 @@ export abstract class MovementExecutor extends Movement {
/**
* TODO: potentially buggy code. Check.
*/
- public async perform(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ public async perform (thisMove: Move, currentIndex: number, path: Move[]): Promise {
log('Performing move at index %d', currentIndex)
this.currentMove = thisMove
if (this.resetReason != null) throw this.resetReason // new ResetError('Movement is resetting.')
@@ -222,17 +221,17 @@ export abstract class MovementExecutor extends Movement {
}
}
- public async _performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ public async _performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
await this.holdUntilAborted(thisMove, this.task)
return await this.performInit(thisMove, currentIndex, path)
}
- public async _performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+ public async _performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
await this.holdUntilAborted(thisMove, this.task)
return await this.performPerTick(thisMove, tickCount, currentIndex, path)
}
- public async _align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
+ public async _align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
await this.holdUntilAborted(thisMove, this.task)
return await this.align(thisMove, tickCount, goal)
}
@@ -243,7 +242,7 @@ export abstract class MovementExecutor extends Movement {
* Perform initial setup upon movement start.
* Can be sync or async.
*/
- abstract performInit(thisMove: Move, currentIndex: number, path: Move[]): void | Promise
+ abstract performInit (thisMove: Move, currentIndex: number, path: Move[]): void | Promise
/**
* Runtime calculation.
@@ -252,7 +251,7 @@ export abstract class MovementExecutor extends Movement {
* Return whether or not bot has reached the goal.
*
*/
- abstract performPerTick(
+ abstract performPerTick (
thisMove: Move,
tickCount: number,
currentIndex: number,
@@ -266,7 +265,7 @@ export abstract class MovementExecutor extends Movement {
* This can be used to align to the center of blocks, etc.
* Align IS allowed to throw exceptions, it will revert to recovery.
*/
- align(thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): boolean | Promise {
+ align (thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): boolean | Promise {
const target = lookTarget ?? thisMove.entryPos
if (lookTarget != null) void this.postInitAlignToPath(thisMove, { lookAt: target })
else void this.postInitAlignToPath(thisMove)
@@ -279,7 +278,7 @@ export abstract class MovementExecutor extends Movement {
*
* Check whether or not the move is already currently completed. This is checked once, before alignment.
*/
- isAlreadyCompleted(thisMove: Move, tickCount: number, goal: goals.Goal): boolean {
+ isAlreadyCompleted (thisMove: Move, tickCount: number, goal: goals.Goal): boolean {
return this.isComplete(thisMove)
}
@@ -291,16 +290,15 @@ export abstract class MovementExecutor extends Movement {
* Does so via velocity direction check (heading towards the block)
* and bounding box check (touching OR slightly above block).
*/
- protected isComplete(startMove: Move, endMove: Move = startMove, opts: CompleteOpts = {}): boolean {
-
+ protected isComplete (startMove: Move, endMove: Move = startMove, opts: CompleteOpts = {}): boolean {
if (this.cI !== undefined) {
if (!this.cI.allowExit) return false
}
- const checkP = opts.checkPlaces ?? true;
- const checkB = opts.checkBreaks ?? true;
- if (checkP && this.toPlaceLen() > 0) return false;
- if (checkB && this.toBreakLen() > 0) return false;
+ const checkP = opts.checkPlaces ?? true
+ const checkB = opts.checkBreaks ?? true
+ if (checkP && this.toPlaceLen() > 0) return false
+ if (checkB && this.toBreakLen() > 0) return false
const ticks = opts.ticks ?? 1
@@ -318,10 +316,10 @@ export abstract class MovementExecutor extends Movement {
const dist = offset.norm()
const ectx = EPhysicsCtx.FROM_BOT(this.bot.physicsUtil.engine, this.bot)
- const history = [ectx.position.clone()];
+ const history = [ectx.position.clone()]
for (let i = 0; i < ticks; i++) {
ectx.state.control.set('jump', false) // we don't want to jump again.
- ectx.state.jumpQueued = false;
+ ectx.state.jumpQueued = false
this.bot.physicsUtil.engine.simulate(ectx, this.world)
history.push(ectx.position.clone())
}
@@ -346,7 +344,6 @@ export abstract class MovementExecutor extends Movement {
!ectx.state.onGround &&
this.bot.pathfinder.world.getBlockInfo(this.bot.entity.position.floored().translate(0, -0.6, 0)).liquid
-
if (aboveWater) {
bb1bl = this.bot.pathfinder.world.getBlockInfo(target.floored())
bbCheckCond = bb1bl.walkthrough
@@ -385,7 +382,7 @@ export abstract class MovementExecutor extends Movement {
if (similarDirection && headingThatWay) return !ectx.state.isCollidedHorizontally // in air check.
else if (dist < 0.2) return true
else {
- return true;
+ return true
}
// console.log('finished!', this.bot.entity.position, endMove.exitPos, bbsVertTouching, similarDirection, headingThatWay, offset.y)
@@ -409,10 +406,8 @@ export abstract class MovementExecutor extends Movement {
)
}
- public isInitAligned(thisMove: Move, target: Vec3 = thisMove.entryPos, options: InitAlignOpts = {}): boolean {
-
- if (this.isComplete(thisMove)) return true;
-
+ public isInitAligned (thisMove: Move, target: Vec3 = thisMove.entryPos, options: InitAlignOpts = {}): boolean {
+ if (this.isComplete(thisMove)) return true
const off0 = thisMove.exitPos.minus(this.bot.entity.position).normalize()
const off1 = thisMove.exitPos.minus(target).normalize()
@@ -426,49 +421,48 @@ export abstract class MovementExecutor extends Movement {
const similarDirection = off0.dot(off1) > 0.7
- let bb0 = options.customBB;
+ let bb0 = options.customBB
if (bb0 == null) {
const normPos = getNormalizedPos(this.bot)
bb0 = AABBUtils.getEntityAABBRaw({ position: normPos, width: 0.6, height: 1.8 })
}
- const toCheck = options.others ?? [];
+ const toCheck = options.others ?? []
const entercheck = { pos: target.offset(0, -1, 0), requireSupport: true }
- const exitCheck = { pos: thisMove.exitPos.floored().translate(0, -1, 0), requireSupport: true };
+ const exitCheck = { pos: thisMove.exitPos.floored().translate(0, -1, 0), requireSupport: true }
toCheck.push(entercheck)
toCheck.push(exitCheck)
- let valid;
+ let valid
// default false.
if (options.enterExitInterp) {
valid = this.interpolatedBBCheck(bb0, thisMove.cachedVec.offset(0.5, 0, 0.5), thisMove.exitPos.floored().offset(0.5, 0, 0.5)) || this.boundingBoxCheck(bb0, ...toCheck)
- } else valid = this.boundingBoxCheck(bb0, ...toCheck);
+ } else valid = this.boundingBoxCheck(bb0, ...toCheck)
if (valid) {
log('isInitAligned: yaw check passed. similarDirection=%s, dist=%d', similarDirection, this.bot.entity.position.xzDistanceTo(target))
if (similarDirection) return true
else {
- log(`exit -> cur: %O, exit -> target: %O`, off0, off1, off0.dot(off1))
+ log('exit -> cur: %O, exit -> target: %O', off0, off1, off0.dot(off1))
if (this.bot.entity.position.xzDistanceTo(target) < 0.3) return true
if (this.boundingBoxCheck(bb0, exitCheck)) return true
}
}
- log(`isInitAligned: We are not aligned. us: %O, target pos: %O`, bb0, target)
- log(`Extra info: opts: %O`, options)
+ log('isInitAligned: We are not aligned. us: %O, target pos: %O', bb0, target)
+ log('Extra info: opts: %O', options)
return false
}
-
/**
* Lazy code.
*/
- public safeToCancel(startMove: Move, endMove: Move = startMove): boolean {
+ public safeToCancel (startMove: Move, endMove: Move = startMove): boolean {
return this.bot.entity.onGround || ((this.bot.entity as any).isInWater as boolean)
}
- protected getTooLowCheckY(pos: Vec3 = this.bot.entity.position): number {
+ protected getTooLowCheckY (pos: Vec3 = this.bot.entity.position): number {
const supportBbs = getUnderlyingBBs(this.world, pos, 0.6)
if (supportBbs.length === 0) {
return pos.y + 0.6
@@ -479,7 +473,7 @@ export abstract class MovementExecutor extends Movement {
return pos.y + 0.6 + supportCorrection
}
- protected tooLowCheck(thisMove: Move, pos: Vec3 = this.bot.entity.position): boolean {
+ protected tooLowCheck (thisMove: Move, pos: Vec3 = this.bot.entity.position): boolean {
const botY = this.getTooLowCheckY(pos)
return botY < thisMove.exitPos.y && botY < thisMove.entryPos.y
}
@@ -490,8 +484,8 @@ export abstract class MovementExecutor extends Movement {
* Return breaks first as they will not interfere with placements,
* whereas placements will almost always interfere with breaks (LOS failure).
*/
- async interactNeeded(ticks = 1): Promise {
- const start = performance.now();
+ async interactNeeded (ticks = 1): Promise {
+ const start = performance.now()
for (const breakTarget of this.currentMove.toBreak) {
if (breakTarget !== this._cI && !breakTarget.done) {
if (!breakTarget.needToPerform(this.bot)) continue
@@ -514,7 +508,7 @@ export abstract class MovementExecutor extends Movement {
/**
* Generalized function to perform an interaction.
*/
- async performInteraction(interaction: PlaceHandler | BreakHandler, opts: InteractOpts = {}): Promise {
+ async performInteraction (interaction: PlaceHandler | BreakHandler, opts: InteractOpts = {}): Promise {
this._cI = interaction
interaction.loadMove(this)
if (interaction instanceof PlaceHandler) {
@@ -524,7 +518,7 @@ export abstract class MovementExecutor extends Movement {
}
}
- protected async performPlace(place: PlaceHandler, opts: InteractOpts = {}): Promise {
+ protected async performPlace (place: PlaceHandler, opts: InteractOpts = {}): Promise {
const item = place.getItem(this.bot)
if (item == null) throw new CancelError('MovementExecutor: no item to place')
log('performPlace: placing item %s', item.name)
@@ -532,7 +526,7 @@ export abstract class MovementExecutor extends Movement {
this._cI = undefined
}
- protected async performBreak(breakTarget: BreakHandler, opts: InteractOpts = {}): Promise {
+ protected async performBreak (breakTarget: BreakHandler, opts: InteractOpts = {}): Promise {
const block = breakTarget.getBlock(this.bot.pathfinder.world)
if (block == null) throw new CancelError('MovementExecutor: no block to break')
const item = breakTarget.getItem(this.bot, block)
@@ -545,7 +539,7 @@ export abstract class MovementExecutor extends Movement {
* Utility function to have the bot look in the direction of the target,
* but only on the xz plane (pitch is always 0).
*/
- public async lookAtPathPos(vec3: Vec3, force = this.settings.forceLook): Promise {
+ public async lookAtPathPos (vec3: Vec3, force = this.settings.forceLook): Promise {
const pos = this.bot.entity.position
const dx = vec3.x - pos.x
const dz = vec3.z - pos.z
@@ -555,17 +549,17 @@ export abstract class MovementExecutor extends Movement {
await this.bot.look(yaw, 0, force)
}
- public async look(yaw: number, pitch: number, force = this.settings.forceLook): Promise {
+ public async look (yaw: number, pitch: number, force = this.settings.forceLook): Promise {
if (this.isLookingAtRotation(yaw, pitch, 0.001)) return
await this.bot.look(yaw, pitch, force)
}
- public async lookAt(vec3: Vec3, force = this.settings.forceLook): Promise {
+ public async lookAt (vec3: Vec3, force = this.settings.forceLook): Promise {
if (this.isLookingAt(vec3, 0.001)) return
await this.bot.lookAt(vec3, force)
}
- public isLookingAt(vec3: Vec3, limit = 0.01): boolean {
+ public isLookingAt (vec3: Vec3, limit = 0.01): boolean {
if (!this.settings.careAboutLookAlignment) return true
const bl = this.bot.blockAtCursor(256) as unknown as RayType | null
@@ -575,7 +569,7 @@ export abstract class MovementExecutor extends Movement {
return bl.intersect.minus(eyePos).normalize().dot(vec3.minus(eyePos).normalize()) > 1 - limit
}
- public isLookingAtYaw(vec3: Vec3, limit = 0.01): boolean {
+ public isLookingAtYaw (vec3: Vec3, limit = 0.01): boolean {
if (!this.settings.careAboutLookAlignment) return true
const eyePos = this.bot.entity.position.offset(0, 1.62, 0)
@@ -587,7 +581,7 @@ export abstract class MovementExecutor extends Movement {
return this.isLookingAtYawRotation(yaw, limit)
}
- public isLookingAtRotation(yaw: number, pitch: number, limit = 0.01): boolean {
+ public isLookingAtRotation (yaw: number, pitch: number, limit = 0.01): boolean {
if (!this.settings.careAboutLookAlignment) return true
const currentYaw = this.bot.entity.yaw
@@ -608,7 +602,7 @@ export abstract class MovementExecutor extends Movement {
return currentDir.dot(targetDir) > 1 - limit
}
- public isLookingAtYawRotation(yaw: number, limit = 0.01): boolean {
+ public isLookingAtYawRotation (yaw: number, limit = 0.01): boolean {
if (!this.settings.careAboutLookAlignment) return true
const currentYaw = this.bot.entity.yaw
@@ -629,22 +623,22 @@ export abstract class MovementExecutor extends Movement {
// Utils for handling collisions
- private boundingBoxCheck(orgBB: AABB, ...info: AlignmentBBInfo[]): boolean {
- let valid = false;
+ private boundingBoxCheck (orgBB: AABB, ...info: AlignmentBBInfo[]): boolean {
+ let valid = false
for (const { pos, requireSupport } of info) {
- const bInfo = this.getBlockInfoRaw(pos);
+ const bInfo = this.getBlockInfoRaw(pos)
const bbs = bInfo.getBBs()
if (bbs.length === 0) bbs.push(AABB.fromBlock(bInfo.position))
if (requireSupport && !(bInfo.physical || bInfo.liquid)) {
- continue;
+ continue
}
valid = valid || bbs.some((b) => b.collides(orgBB)) // should shortcut.
}
- return valid;
+ return valid
}
- private interpolatedBBCheck(orgBB: AABB, start: Vec3, end: Vec3): boolean {
+ private interpolatedBBCheck (orgBB: AABB, start: Vec3, end: Vec3): boolean {
for (const pos of interpolateStepPoints(start, end, 0.8)) {
const bInfo = this.getBlockInfoRaw(pos)
const bbs = bInfo.getBBs()
@@ -656,26 +650,24 @@ export abstract class MovementExecutor extends Movement {
if (bbs.some((b) => b.collides(orgBB))) {
return true
}
-
-
}
- log(`interpolatedBBCheck: no collision. orgBB %O, from %O to %O`, orgBB, start, end)
+ log('interpolatedBBCheck: no collision. orgBB %O, from %O to %O', orgBB, start, end)
return false
}
// Sim functions.
- protected resetState(): PlayerState {
+ protected resetState (): PlayerState {
this.simCtx.state.update(this.bot)
return this.simCtx.state
}
- protected simUntil(...args: Parameters['simulateUntil']>): ReturnType['simulateUntil']> {
+ protected simUntil (...args: Parameters['simulateUntil']>): ReturnType['simulateUntil']> {
this.simCtx.state.update(this.bot)
return this.sim.simulateUntil(...args)
}
- protected simUntilGrounded(controller: Controller, maxTicks = 1000): PlayerState {
+ protected simUntilGrounded (controller: Controller, maxTicks = 1000): PlayerState {
this.simCtx.state.update(this.bot)
return this.sim.simulateUntil(
(state) => state.onGround,
@@ -687,7 +679,7 @@ export abstract class MovementExecutor extends Movement {
)
}
- protected simJump({ goal, controller }: { goal?: SimulationGoal, controller?: Controller } = {}, maxTicks = 1000): PlayerState {
+ protected simJump ({ goal, controller }: { goal?: SimulationGoal, controller?: Controller } = {}, maxTicks = 1000): PlayerState {
this.simCtx.state.update(this.bot)
goal = goal ?? ((state) => state.onGround)
controller =
@@ -698,16 +690,16 @@ export abstract class MovementExecutor extends Movement {
return this.sim.simulateUntil(goal, () => { }, controller, this.simCtx, this.world, maxTicks)
}
- protected async postInitAlignToPath(
+ protected async postInitAlignToPath (
startMove: Move,
opts?: PostInitAlignOpts
): Promise
- protected async postInitAlignToPath(
+ protected async postInitAlignToPath (
startMove: Move,
endMove?: Move,
opts?: PostInitAlignOpts
): Promise
- protected async postInitAlignToPath(startMove: Move, endMove?: any, opts?: any): Promise {
+ protected async postInitAlignToPath (startMove: Move, endMove?: any, opts?: any): Promise {
if (endMove === undefined) {
endMove = startMove
opts = {}
@@ -733,7 +725,7 @@ export abstract class MovementExecutor extends Movement {
} else {
await this.lookAtPathPos(target)
- const yawPitch = posToYawPitchFromEye(this.bot.entity.position, 1.62, target);
+ const yawPitch = posToYawPitchFromEye(this.bot.entity.position, 1.62, target)
if (!this.isLookingAtYaw(target, 0.01)) {
log(`postInitAlignToPath: failed yaw check (offset=${yawPitch.yaw - this.bot.entity.yaw})`)
}
@@ -742,7 +734,6 @@ export abstract class MovementExecutor extends Movement {
botStrafeMovement(this.bot, endMove.exitPos, true)
botSmartMovement(this.bot, endMove.exitPos, sprint)
-
// const orgControl = this.bot.getControlState('sneak')
// if (this.willFallOff(1)) {
// log(`postInitAlignToPath: we are about to fall off! sneaking...`)
@@ -750,33 +741,27 @@ export abstract class MovementExecutor extends Movement {
// } else {
// this.bot.setControlState('sneak', orgControl)
// }
-
}
-
-
-
/**
* @returns whether we fall off.
*/
- public simForward(options: SimOpts = {}): EPhysicsCtx {
- const ectx = options.ectx ?? EPhysicsCtx.FROM_BOT(this.sim.ctx, this.bot);
+ public simForward (options: SimOpts = {}): EPhysicsCtx {
+ const ectx = options.ectx ?? EPhysicsCtx.FROM_BOT(this.sim.ctx, this.bot)
const ticks = options.ticks ?? 1
for (let i = 0; i < ticks; i++) {
-
- if (options.controls) {
- ectx.state.control = options.controls;
+ if (options.controls != null) {
+ ectx.state.control = options.controls
}
- this.sim.ctx.simulate(ectx, this.bot.world);
+ this.sim.ctx.simulate(ectx, this.bot.world)
}
return ectx
}
-
- protected doWaterLogic(): boolean {
+ protected doWaterLogic (): boolean {
if ((this.bot.entity as any).isInWater as boolean) return true
// potentially false, if underwater + standing on ground.
@@ -785,5 +770,4 @@ export abstract class MovementExecutor extends Movement {
const bl = this.getBlockInfo(this.bot.entity.position, 0, -0.6, 0)
return bl.liquid
}
-
}
diff --git a/src/mineflayer-specific/movements/movementExecutors.ts b/src/mineflayer-specific/movements/movementExecutors.ts
index 6a13d57..3c0cda2 100644
--- a/src/mineflayer-specific/movements/movementExecutors.ts
+++ b/src/mineflayer-specific/movements/movementExecutors.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires, @typescript-eslint/strict-boolean-expressions, @typescript-eslint/restrict-template-expressions, @typescript-eslint/explicit-function-return-type */
import { Vec3 } from 'vec3'
import * as goals from '../goals'
import { Move } from '../move'
@@ -22,24 +23,23 @@ const logDown = debug('minecraft-pathfinding:movementExecutors:StraightDown')
const logUp = debug('minecraft-pathfinding:movementExecutors:StraightUp')
const logParkour = debug('minecraft-pathfinding:movementExecutors:Parkour')
-
export class IdleMovementExecutor extends MovementExecutor {
- provideMovements(start: Move, storage: Move[]): void { }
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ provideMovements (start: Move, storage: Move[]): void { }
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
logIdle('performInit called')
}
- async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+
+ async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
return true
}
}
-
export class NewForwardExecutor extends MovementExecutor {
- protected isComplete(startMove: Move, endMove?: Move, opts?: CompleteOpts): boolean {
+ protected isComplete (startMove: Move, endMove?: Move, opts?: CompleteOpts): boolean {
return super.isComplete(startMove, endMove, { ticks: 2 })
}
- private async faceForward(): Promise {
+ private async faceForward (): Promise {
// console.log('called faceForward!')
if (this.doWaterLogic()) return true
const eyePos = this.bot.entity.position.offset(0, this.bot.entity.height, 0)
@@ -51,9 +51,8 @@ export class NewForwardExecutor extends MovementExecutor {
return this.currentMove?.toPlace.length === 0 || !near
}
- override async align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
+ override async align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
if (this.doWaterLogic()) {
-
// clear to remove jump.
this.bot.clearControlStates()
return await super.align(thisMove, tickCount, goal)
@@ -72,7 +71,7 @@ export class NewForwardExecutor extends MovementExecutor {
return await this.landAlign(thisMove, tickCount, goal)
}
- async landAlign(thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
+ async landAlign (thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
const faceForward = await this.faceForward()
const opts: InitAlignOpts = { enterExitInterp: true }
@@ -90,7 +89,6 @@ export class NewForwardExecutor extends MovementExecutor {
this.bot.setControlState('forward', true)
if (this.bot.food <= 6) this.bot.setControlState('sprint', false)
else this.bot.setControlState('sprint', true)
-
} else {
const offset = this.bot.entity.position.minus(target).plus(this.bot.entity.position)
// await this.postInitAlignToPath(thisMove, { lookAt: offset })
@@ -107,7 +105,7 @@ export class NewForwardExecutor extends MovementExecutor {
return this.isInitAligned(thisMove, target, opts)
}
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
// console.log('ForwardMove', thisMove.exitPos, thisMove.toPlace.length, thisMove.toBreak.length)
this.bot.clearControlStates()
@@ -124,7 +122,7 @@ export class NewForwardExecutor extends MovementExecutor {
}
// TODO: clean this up.
- private canJump(thisMove: Move, currentIndex: number, path: Move[]): boolean {
+ private canJump (thisMove: Move, currentIndex: number, path: Move[]): boolean {
if (this.doWaterLogic()) {
if (this.bot.entity.position.y < thisMove.exitPos.y) {
return true
@@ -178,7 +176,7 @@ export class NewForwardExecutor extends MovementExecutor {
return ctx.state.onGround
}
- async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+ async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot))) {
return false
} else if (this.cI == null) {
@@ -224,12 +222,11 @@ export class ForwardJumpExecutor extends MovementExecutor {
private readonly shitter: JumpCalculator = new JumpCalculator(this.sim, this.bot, this.world, this.simCtx)
private flag = false
-
- protected isComplete(startMove: Move, endMove?: Move): boolean {
+ protected isComplete (startMove: Move, endMove?: Move): boolean {
return super.isComplete(startMove, endMove, { ticks: 0 })
}
- override async align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
+ override async align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
if (super.doWaterLogic()) {
this.bot.setControlState('jump', this.bot.entity.position.y < thisMove.entryPos.y)
}
@@ -237,7 +234,7 @@ export class ForwardJumpExecutor extends MovementExecutor {
return await super.align(thisMove, tickCount, goal)
}
- align1(thisMove: Move, tickCount: number, goal: goals.Goal): boolean {
+ align1 (thisMove: Move, tickCount: number, goal: goals.Goal): boolean {
const bb = AABBUtils.getEntityAABBRaw({ position: this.bot.entity.position, width: 0.6, height: 1.8 })
if (this.flag) {
@@ -263,11 +260,11 @@ export class ForwardJumpExecutor extends MovementExecutor {
return false
}
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
this.flag = false
this.bot.clearControlStates()
- logJump(`performInit`)
+ logJump('performInit')
if (thisMove.toBreak.length > 0) {
await this.bot.clearControlStates()
@@ -287,9 +284,9 @@ export class ForwardJumpExecutor extends MovementExecutor {
}
}
- async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+ async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot))) {
- // NOTE: During a jump, we DO NOT want to clear control states or set sneak,
+ // NOTE: During a jump, we DO NOT want to clear control states or set sneak,
// otherwise the bot will lose its momentum and fall straight down!
return false
}
@@ -362,7 +359,7 @@ export class ForwardJumpExecutor extends MovementExecutor {
}
export class NewForwardJumpExecutor extends ForwardJumpExecutor {
- override async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+ override async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
if (super.doWaterLogic()) {
this.bot.setControlState('jump', this.bot.entity.position.y < thisMove.exitPos.y)
void this.postInitAlignToPath(thisMove)
@@ -374,24 +371,22 @@ export class NewForwardJumpExecutor extends ForwardJumpExecutor {
}
export class ForwardDropDownExecutor extends MovementExecutor {
-
- private handleSneak(thisMove: Move) {
+ private handleSneak (thisMove: Move) {
if (super.doWaterLogic()) {
this.bot.setControlState('sneak', this.bot.entity.position.y > thisMove.exitPos.y && !this.bot.entity.onGround)
}
}
- override async align(thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): Promise {
+ override async align (thisMove: Move, tickCount?: number, goal?: goals.Goal, lookTarget?: Vec3): Promise {
this.handleSneak(thisMove)
return await super.align(thisMove, tickCount, goal, lookTarget)
}
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
await this.postInitAlignToPath(thisMove)
}
-
- async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+ async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot, 0))) {
// If we are locked by an interaction, freeze so we don't accidentally fall early.
this.bot.clearControlStates()
@@ -438,7 +433,6 @@ export class ForwardDropDownExecutor extends MovementExecutor {
}
}
-
if (currentIndex < path.length) void this.postInitAlignToPath(thisMove)
else void this.postInitAlignToPath(thisMove)
@@ -447,10 +441,8 @@ export class ForwardDropDownExecutor extends MovementExecutor {
}
}
-
export class StraightDownExecutor extends MovementExecutor {
-
- align(thisMove: Move): boolean {
+ align (thisMove: Move): boolean {
this.bot.clearControlStates()
const xzVel = this.bot.entity.velocity.offset(0, -this.bot.entity.velocity.y, 0)
if (this.bot.entity.position.xzDistanceTo(thisMove.exitPos) < 0.2 && xzVel.norm() < 0.1) {
@@ -475,11 +467,11 @@ export class StraightDownExecutor extends MovementExecutor {
return false
}
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
// Initialization logic cleared; breaks are dynamically handled in performPerTick
}
- async performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
+ async performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): Promise {
if (this.cI != null && !(await this.cI.allowExternalInfluence(this.bot, 0))) {
this.bot.clearControlStates()
return false
@@ -511,33 +503,33 @@ export class StraightUpExecutor extends MovementExecutor {
private static readonly STOP_DIST = 0.08
private static readonly STOP_SPEED = 0.03
- private _getEntryCenter(thisMove: Move): Vec3 {
+ private _getEntryCenter (thisMove: Move): Vec3 {
return thisMove.entryPos.floored().offset(0.5, 0, 0.5)
}
- private _getExitCenter(thisMove: Move): Vec3 {
+ private _getExitCenter (thisMove: Move): Vec3 {
return thisMove.exitPos.floored().offset(0.5, 0, 0.5)
}
- private _getHorizontalOffsetTo(center: Vec3): Vec3 {
+ private _getHorizontalOffsetTo (center: Vec3): Vec3 {
const pos = this.bot.entity.position
return new Vec3(center.x - pos.x, 0, center.z - pos.z)
}
- private _getHorizontalVelocity(): Vec3 {
+ private _getHorizontalVelocity (): Vec3 {
const vel = this.bot.entity.velocity
return new Vec3(vel.x, 0, vel.z)
}
- private _isHorizontallyCentered(thisMove: Move): boolean {
+ private _isHorizontallyCentered (thisMove: Move): boolean {
return this.bot.entity.position.xzDistanceTo(this._getEntryCenter(thisMove)) <= StraightUpExecutor.CENTER_EPS
}
- private _isHorizontalMotionSmall(): boolean {
+ private _isHorizontalMotionSmall (): boolean {
return this._getHorizontalVelocity().norm() <= StraightUpExecutor.STOP_SPEED
}
- private _facePoint(point: Vec3): void {
+ private _facePoint (point: Vec3): void {
const pos = this.bot.entity.position
const dx = point.x - pos.x
const dz = point.z - pos.z
@@ -551,7 +543,7 @@ export class StraightUpExecutor extends MovementExecutor {
* Using a target slightly "behind" current travel helps botSmartMovement decide
* to hold back when we would otherwise overshoot from forward momentum.
*/
- private _getAirborneCorrectionTarget(thisMove: Move): Vec3 {
+ private _getAirborneCorrectionTarget (thisMove: Move): Vec3 {
const center = this._getEntryCenter(thisMove)
const pos = this.bot.entity.position
const vel = this._getHorizontalVelocity()
@@ -579,7 +571,7 @@ export class StraightUpExecutor extends MovementExecutor {
* - use smart forward/back + strict strafing
* - sprint only when farther away
*/
- private _applyGroundCentering(thisMove: Move): boolean {
+ private _applyGroundCentering (thisMove: Move): boolean {
const center = this._getEntryCenter(thisMove)
const offset = this._getHorizontalOffsetTo(center)
const dist = offset.norm()
@@ -606,7 +598,7 @@ export class StraightUpExecutor extends MovementExecutor {
* - allow smartMovement to choose forward vs back
* - allow strict strafe to counter lateral drift
*/
- private _applyVerticalAscentControls(thisMove: Move): boolean {
+ private _applyVerticalAscentControls (thisMove: Move): boolean {
const center = this._getEntryCenter(thisMove)
const correctionTarget = this._getAirborneCorrectionTarget(thisMove)
const distToCenter = this._getHorizontalOffsetTo(center).norm()
@@ -629,12 +621,11 @@ export class StraightUpExecutor extends MovementExecutor {
return false
}
-
- isAlreadyCompleted(thisMove: Move, tickCount: number, goal: goals.Goal): boolean {
+ isAlreadyCompleted (thisMove: Move, tickCount: number, goal: goals.Goal): boolean {
return this.bot.entity.position.y >= thisMove.exitPos.y
}
- override async align(thisMove: Move): Promise {
+ override async align (thisMove: Move): Promise {
const inWater = (this.bot.entity as any).isInWater as boolean
if (!this.bot.entity.onGround || inWater) {
@@ -644,7 +635,7 @@ export class StraightUpExecutor extends MovementExecutor {
return this._applyGroundCentering(thisMove)
}
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
this.bot.clearControlStates()
for (const breakH of this.toBreak()) {
@@ -664,7 +655,7 @@ export class StraightUpExecutor extends MovementExecutor {
}
}
- performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise {
+ performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise {
if (this.bot.entity.position.y < thisMove.entryPos.y) {
throw new CancelError('StraightUp: too low')
}
@@ -704,31 +695,30 @@ export class ParkourForwardExecutor extends MovementExecutor {
protected static readonly APPROACH_YAW_EPS: number = 0.16 // ~9.2 deg — generous for cardinal jumps
- protected isComplete(startMove: Move, endMove?: Move, opts: CompleteOpts = {}): boolean {
+ protected isComplete (startMove: Move, endMove?: Move, opts: CompleteOpts = {}): boolean {
const ret = super.isComplete(startMove, endMove, opts)
- return ret;
+ return ret
}
- private _debugLog(...args: any[]): void {
+ private _debugLog (...args: any[]): void {
logParkour(...args)
}
- private _lockCurrentYaw(targetYaw: number): void {
+ private _lockCurrentYaw (targetYaw: number): void {
this.lockedYaw = targetYaw
}
- private _clearLockedYaw(): void {
+ private _clearLockedYaw (): void {
this.lockedYaw = null
}
- private _applyLockedYaw(): void {
-
+ private _applyLockedYaw (): void {
if (this.lockedYaw != null) {
this.bot.entity.yaw = this.lockedYaw
}
}
- private _applySmartControls(target: Vec3, jump: boolean): void {
+ private _applySmartControls (target: Vec3, jump: boolean): void {
this._applyLockedYaw()
botSmartMovement(this.bot, target, true)
botStrafeMovement(this.bot, target, true)
@@ -736,12 +726,11 @@ export class ParkourForwardExecutor extends MovementExecutor {
this.bot.setControlState('sneak', false)
}
- private _queueLookAtSync(target: Vec3): Promise {
-
+ private async _queueLookAtSync (target: Vec3): Promise {
this._pendingLookTarget = target.offset(0, this.bot.entity.position.y - target.y, 0)
if (this._lookAtInFlight != null) {
- return this._lookAtInFlight
+ return await this._lookAtInFlight
}
this._lookAtInFlight = (async () => {
@@ -756,24 +745,24 @@ export class ParkourForwardExecutor extends MovementExecutor {
}
})()
- return this._lookAtInFlight
+ return await this._lookAtInFlight
}
- private _clearBackupState(): void {
+ private _clearBackupState (): void {
this.backingUp = false
this.backupSettling = false
this.backupTarget = null
}
- private _getTargetBlock(thisMove: Move): Vec3 {
+ private _getTargetBlock (thisMove: Move): Vec3 {
return thisMove.exitPos.offset(0, -1, 0)
}
- private _getTargetEyeVec(target: Vec3): Vec3 {
+ private _getTargetEyeVec (target: Vec3): Vec3 {
return this.shitterTwo.findGoalVertex(AABB.fromBlockPos(target))
}
- private _getUnderlyingBbs(thisMove: Move): AABB[] {
+ private _getUnderlyingBbs (thisMove: Move): AABB[] {
const bbs = getUnderlyingBBs(this.world, this.bot.entity.position, 0.6)
if (bbs.length === 0) {
bbs.push(AABB.fromBlockPos(thisMove.entryPos.offset(0, -1, 0)))
@@ -781,7 +770,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
return bbs
}
- private _getBackupTarget(thisMove: Move): Vec3 | null {
+ private _getBackupTarget (thisMove: Move): Vec3 | null {
const target = this._getTargetBlock(thisMove)
const targetEyeVec = this._getTargetEyeVec(target)
@@ -803,22 +792,19 @@ export class ParkourForwardExecutor extends MovementExecutor {
return backupTarget
}
- private _shouldSneakDuringBackup(thisMove: Move, target: Vec3): boolean {
+ private _shouldSneakDuringBackup (thisMove: Move, target: Vec3): boolean {
const entryBB = AABB.fromBlockPos(thisMove.entryPos)
const botPos = this.bot.entity.position
- if (botPos.xzDistanceTo(target) < 0.1) return true;
-
+ if (botPos.xzDistanceTo(target) < 0.1) return true
const controls = ControlStateHandler.COPY_BOT(this.bot).set('sneak', false).set('jump', false)
const ectx = this.simForward({ ticks: 2, controls })
// console.log(ectx.state.pos, ectx.state.control, ectx.state.pos.y, this.bot.entity.position.y, !ectx.state.onGround)
return ectx.state.pos.y < this.bot.entity.position.y && !ectx.state.onGround
-
-
}
- private _applyBackupControls(thisMove: Move, target: Vec3): void {
+ private _applyBackupControls (thisMove: Move, target: Vec3): void {
this._lockCurrentYaw(this._desiredYawTo(target))
void this._queueLookAtSync(target)
this._applySmartControls(target, false)
@@ -826,7 +812,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
this.bot.setControlState('sneak', this._shouldSneakDuringBackup(thisMove, target))
}
- private _startBackup(thisMove: Move): boolean {
+ private _startBackup (thisMove: Move): boolean {
this.backupTarget ??= this._getBackupTarget(thisMove)
if (this.backupTarget == null) {
this._debugLog('backup start failed: no backup target')
@@ -840,7 +826,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
return true
}
- private _advanceBackup(thisMove: Move): 'backing' | 'recheck' | 'jump' | 'failed' {
+ private _advanceBackup (thisMove: Move): 'backing' | 'recheck' | 'jump' | 'failed' {
this.backupTarget ??= this._getBackupTarget(thisMove)
if (this.backupTarget == null) {
// this._debugLog('backup advance failed: no backup target')
@@ -893,7 +879,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
return 'recheck'
}
- private _getJumpState(thisMove: Move): {
+ private _getJumpState (thisMove: Move): {
target: Vec3
targetEyeVec: Vec3
canDirectJump: boolean
@@ -913,7 +899,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
}
}
- private _debugJumpState(
+ private _debugJumpState (
label: string,
jumpState: {
target: Vec3
@@ -923,7 +909,6 @@ export class ParkourForwardExecutor extends MovementExecutor {
fallOffEdge: boolean
}
): void {
-
(this as any)._lastTime ??= 0
this._debugLog(label, performance.now() - (this as any)._lastTime)
@@ -937,7 +922,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
'current bot info:',
this.bot.entity.yaw,
this.bot.entity.position,
- this.bot.entity.velocity,
+ this.bot.entity.velocity
)
this._debugLog(
'yaw info:',
@@ -948,7 +933,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
(this as any)._lastTime = performance.now()
}
- private _clearApproachControls(): void {
+ private _clearApproachControls (): void {
this.bot.setControlState('forward', false)
this.bot.setControlState('back', false)
this.bot.setControlState('left', false)
@@ -958,31 +943,31 @@ export class ParkourForwardExecutor extends MovementExecutor {
this.bot.setControlState('sneak', false)
}
- private _startJumpExecution(target: Vec3): void {
+ private _startJumpExecution (target: Vec3): void {
this.lockedYaw = this._desiredYawTo(target)
this.executing = true
this._applySmartControls(target, true)
}
- protected _desiredYawTo(target: Vec3): number {
+ protected _desiredYawTo (target: Vec3): number {
const dx = target.x - this.bot.entity.position.x
const dz = target.z - this.bot.entity.position.z
return Math.atan2(-dx, -dz)
}
- protected _yawDeltaAbs(targetYaw: number): number {
+ protected _yawDeltaAbs (targetYaw: number): number {
let delta = targetYaw - this.bot.entity.yaw
while (delta > Math.PI) delta -= Math.PI * 2
while (delta < -Math.PI) delta += Math.PI * 2
return Math.abs(delta)
}
- protected _isYawAlignedForApproach(target: Vec3): boolean {
+ protected _isYawAlignedForApproach (target: Vec3): boolean {
const wantedYaw = this._desiredYawTo(target)
return this._yawDeltaAbs(wantedYaw) <= ParkourForwardExecutor.APPROACH_YAW_EPS
}
- private _tryApproachWhenAligned(targetEyeVec: Vec3): boolean {
+ private _tryApproachWhenAligned (targetEyeVec: Vec3): boolean {
void this._queueLookAtSync(targetEyeVec)
if (!this._isYawAlignedForApproach(targetEyeVec)) {
@@ -994,7 +979,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
return true
}
- async align(thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
+ async align (thisMove: Move, tickCount: number, goal: goals.Goal): Promise {
this.executing = false
this._clearLockedYaw()
@@ -1071,7 +1056,7 @@ export class ParkourForwardExecutor extends MovementExecutor {
}
}
- async performInit(thisMove: Move, currentIndex: number, path: Move[]): Promise {
+ async performInit (thisMove: Move, currentIndex: number, path: Move[]): Promise {
this._clearLockedYaw()
this._clearBackupState()
this.backupAttempted = false
@@ -1081,11 +1066,10 @@ export class ParkourForwardExecutor extends MovementExecutor {
void this._queueLookAtSync(targetEyeVec)
}
- performPerTick(thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise {
+ performPerTick (thisMove: Move, tickCount: number, currentIndex: number, path: Move[]): boolean | Promise {
const target = this._getTargetBlock(thisMove)
const targetEyeVec = this._getTargetEyeVec(target)
-
if (this.executing) {
// printBotControls(this.bot, logParkour)
this._lockCurrentYaw(this._desiredYawTo(targetEyeVec))
@@ -1163,7 +1147,7 @@ export class ParkourDiagonalExecutor extends ParkourForwardExecutor {
* already reads `ParkourForwardExecutor.APPROACH_YAW_EPS`, so we
* override the instance method to point at the subclass constant instead.
*/
- protected override _isYawAlignedForApproach(target: Vec3): boolean {
+ protected override _isYawAlignedForApproach (target: Vec3): boolean {
const wantedYaw = this._desiredYawTo(target)
return this._yawDeltaAbs(wantedYaw) <= ParkourDiagonalExecutor.APPROACH_YAW_EPS
}
diff --git a/src/mineflayer-specific/movements/movementProvider.ts b/src/mineflayer-specific/movements/movementProvider.ts
index 7558048..2bc1ae0 100644
--- a/src/mineflayer-specific/movements/movementProvider.ts
+++ b/src/mineflayer-specific/movements/movementProvider.ts
@@ -2,7 +2,7 @@ import { Bot } from 'mineflayer'
import { Move } from '../move'
import * as goals from '../goals'
import { World } from '../world/worldInterface'
-import { DEFAULT_MOVEMENT_OPTS, Movement, MovementOptions } from './movement'
+import { Movement, MovementOptions, buildMovementOptions } from './movement'
import { MovementProvider as AMovementProvider } from '../../abstract'
import type { ExecutorMap } from '.'
@@ -91,7 +91,6 @@ export abstract class MovementProvider extends Movement {
}
private getBlockInfoAt (x: number, y: number, z: number, pos?: Vec3): BlockInfo {
-
const wantedDx = x - this.orgX + this.halfX
const wantedDz = z - this.orgZ + this.halfZ
const wantedDy = y - this.orgY + this.halfY
@@ -137,7 +136,7 @@ export class MovementHandler implements AMovementProvider {
recMovement: ExecutorMap,
settings: Partial = {}
): MovementHandler {
- const opts = Object.assign({}, DEFAULT_MOVEMENT_OPTS, settings)
+ const opts = buildMovementOptions(settings)
return new MovementHandler(
bot,
world,
diff --git a/src/mineflayer-specific/movements/movementProviders.ts b/src/mineflayer-specific/movements/movementProviders.ts
index 059c356..6defc66 100644
--- a/src/mineflayer-specific/movements/movementProviders.ts
+++ b/src/mineflayer-specific/movements/movementProviders.ts
@@ -67,6 +67,10 @@ export class Forward extends MovementProvider {
if ((cost += this.safeOrBreak(blockB, toBreak)) > COST_INF) return
if ((cost += this.safeOrBreak(blockC, toBreak)) > COST_INF) return
+ // Exclusion zones: blockC is where the bot's feet end up. Fold the step cost
+ // in now (before the move exists) so Move.cost stays read-only.
+ if ((cost += this.exclusionStep(blockC)) > COST_INF) return
+
// set cachedVec to center of wanted block
neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), start, this, toPlace, toBreak))
}
@@ -133,6 +137,9 @@ export class Diagonal extends MovementProvider {
cost += this.safeOrBreak(this.getBlockInfo(node, 0, 1, dir.z), toBreak)
if (cost > COST_INF) return
+ // Exclusion zones: block0 is the destination foot block.
+ if ((cost += this.exclusionStep(block0)) > COST_INF) return
+
neighbors.push(Move.fromPrevious(cost, block0.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak))
}
}
@@ -198,8 +205,6 @@ export class ForwardJump extends MovementProvider {
if ((cost += this.breakCost(blockD)) > COST_INF) return
toBreak.push(BreakHandler.fromVec(blockD.position, 'solid'))
}
- // cost += this.exclusionPlace(blockD)
-
if ((cost += this.safeOrPlace(blockD, toPlace, 'solid')) > COST_INF) return
}
@@ -221,6 +226,9 @@ export class ForwardJump extends MovementProvider {
if ((cost += this.safeOrBreak(blockH, toBreak)) > COST_INF) return
if (toPlace.length > 0) return
+ // Exclusion zones: blockB is the destination foot block.
+ if ((cost += this.exclusionStep(blockB)) > COST_INF) return
+
// set cachedVec to center of block we want.
neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak))
}
@@ -316,6 +324,9 @@ export class ForwardDropDown extends DropDownProvider {
if ((cost += this.safeOrBreak(blockC, toBreak)) > COST_INF) return
if ((cost += this.safeOrBreak(blockD, toBreak)) > COST_INF) return
+ // Exclusion zones: blockLand is where the bot lands and stands.
+ if ((cost += this.exclusionStep(blockLand)) > COST_INF) return
+
// cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities
neighbors.push(Move.fromPrevious(cost, blockLand.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak))
}
@@ -351,6 +362,9 @@ export class StraightDown extends DropDownProvider {
if ((cost += this.safeOrBreak(block1, toBreak)) > COST_INF) return
+ // Exclusion zones: blockLand is where the bot lands and stands.
+ if ((cost += this.exclusionStep(blockLand)) > COST_INF) return
+
// cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities
neighbors.push(Move.fromPrevious(cost, blockLand.position.offset(0.5, 0, 0.5), node, this, toPlace, toBreak))
@@ -377,6 +391,7 @@ export class StraightUp extends MovementProvider {
// if (this.getNumEntitiesAt(node, 0, 0, 0) > 0) return // an entity (besides the player) is blocking the building area
const block2 = this.getBlockInfo(node, 0, 2, 0)
+ const block3 = this.getBlockInfo(node, 0, 1, 0) // the block the bot ends up standing in
const toBreak: BreakHandler[] = []
const toPlace: PlaceHandler[] = []
@@ -384,7 +399,6 @@ export class StraightUp extends MovementProvider {
if ((cost += this.safeOrBreak(block2, toBreak)) > COST_INF) return
if (!block1.climbable) {
- const block3 = this.getBlockInfo(node, 0, 1, 0)
if (!block3.liquid) {
if (!this.settings.allow1by1towers || node.remainingBlocks <= 0) return // not enough blocks to place
@@ -402,6 +416,9 @@ export class StraightUp extends MovementProvider {
}
}
+ // Exclusion zones: block3 is where the bot's feet end up.
+ if ((cost += this.exclusionStep(block3)) > COST_INF) return
+
neighbors.push(Move.fromPrevious(cost, block1.position.offset(0.5, 1, 0.5), node, this, toPlace, toBreak))
}
}
@@ -447,7 +464,7 @@ export class ParkourForward extends MovementProvider {
const maxD = this.settings.allowSprinting ? 5 : 2
for (let d = 2; d <= maxD; d++) {
- const cost = cost0 + d * 0.5 // 0.5 per block forward
+ let cost = cost0 + d * 0.5 // 0.5 per block forward
const dx = dir.x * d
const dz = dir.z * d
@@ -470,31 +487,28 @@ export class ParkourForward extends MovementProvider {
// Down
const blockE = this.getBlockInfo(node, dx, -2, dz)
if (blockE.physical) { // TODO: support jumping into liquid.
- // cost += this.exclusionStep(blockD)
- // cost += this.getNumEntitiesAt(blockD.position, 0, 0, 0) * this.entityCost
- neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this))
- // neighbors.push(new Move(blockD.position.x, blockD.position.y, blockD.position.z, node.remainingBlocks, cost, [], [], true))
+ // Exclusion zones: blockD is the landing. Skip the move if it is forbidden.
+ if ((cost += this.exclusionStep(blockD)) < COST_INF) {
+ neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this))
+ }
}
floorCleared = floorCleared && !blockE.physical
} else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical) {
- // if (d === 5) continue
- const cost1 = cost + 3 // potential slowdown (will fix later.)
- // cost += this.exclusionStep(blockB)
// Forward
-
- neighbors.push(Move.fromPrevious(cost1, blockC.position.offset(0.5, 0, 0.5), node, this))
- // neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks, cost, [], [], true))
+ cost += 3 // potential slowdown (will fix later.)
+ if ((cost += this.exclusionStep(blockC)) < COST_INF) {
+ neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), node, this))
+ }
break
} else if (flag2 && ceilingClear && blockA.walkthrough && blockB.walkthrough && blockC.physical) {
// Up
if (d === 5) continue
// 4 Blocks forward 1 block up is very difficult and fails often
- // cost += this.exclusionStep(blockA)
if (blockC.height - block0.height > 1.2) break // Too high to jump
- // cost += this.getNumEntitiesAt(blockB.position, 0, 0, 0) * this.entityCost
- neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this))
- // neighbors.push(new Move(blockB.position.x, blockB.position.y, blockB.position.z, node.remainingBlocks, cost, [], [], true))
+ if ((cost += this.exclusionStep(blockB)) < COST_INF) {
+ neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this))
+ }
break
// }
} else if (!blockB.walkthrough || !blockC.walkthrough) {
@@ -572,7 +586,7 @@ export class ParkourDiagonal extends MovementProvider {
const dz = dir.z * zSteps
const travel = Math.sqrt(xSteps * xSteps + zSteps * zSteps)
- const cost = cost0 + 0.5 * Diagonal.diagonalCost * travel
+ let cost = cost0 + 0.5 * Diagonal.diagonalCost * travel
const majorIsX = xSteps > zSteps
const majorIsZ = zSteps > xSteps
const frontDx = dx - (majorIsX || xSteps === zSteps ? dir.x : 0)
@@ -609,16 +623,23 @@ export class ParkourDiagonal extends MovementProvider {
if (flag0 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.walkthrough && blockFrontD.walkthrough && !floorCleared) {
if (blockE.physical) {
- neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this))
+ if ((cost += this.exclusionStep(blockD)) < COST_INF) {
+ neighbors.push(Move.fromPrevious(cost, blockD.position.offset(0.5, 0, 0.5), node, this))
+ }
return true
}
} else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical && blockFrontC.walkthrough) {
- neighbors.push(Move.fromPrevious(cost + 3, blockC.position.offset(0.5, 0, 0.5), node, this))
+ cost += 3
+ if ((cost += this.exclusionStep(blockC)) < COST_INF) {
+ neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), node, this))
+ }
return true
} else if (flag2 && ceilingClear && blockA.walkthrough && blockB.walkthrough && blockC.physical && blockFrontB.walkthrough) {
if (blockC.height - block0.height > 1.2) return false
if (travel > PARKOUR_DIAGONAL_3_3_TRAVEL) return false
- neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this))
+ if ((cost += this.exclusionStep(blockB)) < COST_INF) {
+ neighbors.push(Move.fromPrevious(cost, blockB.position.offset(0.5, 0, 0.5), node, this))
+ }
return true
}
diff --git a/src/mineflayer-specific/movements/movementUtils.ts b/src/mineflayer-specific/movements/movementUtils.ts
index 35c2b74..ad27f78 100644
--- a/src/mineflayer-specific/movements/movementUtils.ts
+++ b/src/mineflayer-specific/movements/movementUtils.ts
@@ -21,7 +21,7 @@ interface JumpInfo {
backTick: number
}
-export function stateLookAt(state: IEntityState, point: Vec3): void {
+export function stateLookAt (state: IEntityState, point: Vec3): void {
const delta = point.minus(state.pos.offset(0, state.height - 0.18, 0))
const yaw = Math.atan2(-delta.x, -delta.z)
const groundDistance = Math.sqrt(delta.x * delta.x + delta.z * delta.z)
@@ -30,7 +30,7 @@ export function stateLookAt(state: IEntityState, point: Vec3): void {
state.pitch = pitch
}
-export function isBlockTypeInChunks(info: Block | number, ...chunks: PCChunk[]): boolean {
+export function isBlockTypeInChunks (info: Block | number, ...chunks: PCChunk[]): boolean {
info = info instanceof Number ? info : (info as Block).stateId ?? -1
for (const chunk of chunks) {
@@ -45,7 +45,7 @@ export function isBlockTypeInChunks(info: Block | number, ...chunks: PCChunk[]):
return false
}
-export function getUnderlyingBBs(world: World, pos: Vec3, width: number, colliding = true): AABB[] {
+export function getUnderlyingBBs (world: World, pos: Vec3, width: number, colliding = true): AABB[] {
const verts = [
pos.offset(-width / 2, -0.6, -width / 2),
pos.offset(-width / 2, -0.6, width / 2),
@@ -77,7 +77,7 @@ export function getUnderlyingBBs(world: World, pos: Vec3, width: number, collidi
// type FallReason = 'coyote' | 'yChange' | 'none'
-export function leavingBlockLevel(bot: Bot, world: World, ticks = 1, ectx?: EPhysicsCtx): boolean {
+export function leavingBlockLevel (bot: Bot, world: World, ticks = 1, ectx?: EPhysicsCtx): boolean {
const bbs = getUnderlyingBBs(world, bot.entity.position, 0.6)
const minY = bbs.reduce((acc, bb) => Math.min(acc, bb.minY), Infinity)
@@ -115,14 +115,14 @@ export class JumpCalculator {
ctx: EPhysicsCtx
readonly world: World
- constructor(sim: BaseSimulator, bot: Bot, world: World, ctx: EPhysicsCtx) {
+ constructor (sim: BaseSimulator, bot: Bot, world: World, ctx: EPhysicsCtx) {
this.engine = sim
this.bot = bot
this.ctx = ctx
this.world = world
}
- public findJumpPoint(goal: Vec3, maxTicks = 20): JumpInfo | null {
+ public findJumpPoint (goal: Vec3, maxTicks = 20): JumpInfo | null {
if (this.checkImmediateSprintJump(goal)) {
return { jumpTick: 0, sprintTick: 0, backTick: Infinity }
}
@@ -167,7 +167,7 @@ export class JumpCalculator {
return null
}
- protected resetState(): PlayerState {
+ protected resetState (): PlayerState {
this.ctx = EPhysicsCtx.FROM_BOT(this.engine.ctx, this.bot)
this.ctx.state.age = 0
this.ctx.state.control = ControlStateHandler.DEFAULT()
@@ -176,7 +176,7 @@ export class JumpCalculator {
return this.ctx.state
}
- protected checkImmediateSprintJump(goal: Vec3): boolean {
+ protected checkImmediateSprintJump (goal: Vec3): boolean {
const state = this.resetState()
stateLookAt(state, goal)
this.simJump(state)
@@ -186,7 +186,7 @@ export class JumpCalculator {
return false
}
- protected checkSprintJump(goal: Vec3, firstTicks = 0, secondTicks = 0, sprintAfterJump = false, backTicks = Infinity): boolean {
+ protected checkSprintJump (goal: Vec3, firstTicks = 0, secondTicks = 0, sprintAfterJump = false, backTicks = Infinity): boolean {
const state = this.resetState()
stateLookAt(state, goal)
this.simJumpAdvanced(state, goal, {
@@ -215,7 +215,7 @@ export class JumpCalculator {
return false
}
- protected simJump(state: PlayerState, maxTicks = 20): PlayerState {
+ protected simJump (state: PlayerState, maxTicks = 20): PlayerState {
state.control.set('forward', true)
state.control.set('jump', true)
state.control.set('sprint', true)
@@ -230,7 +230,7 @@ export class JumpCalculator {
return state
}
- protected simJumpAdvanced(
+ protected simJumpAdvanced (
state: PlayerState,
goal: Vec3,
opts: {
@@ -309,13 +309,13 @@ export class ParkourJumpHelper {
private readonly bot: Bot
private readonly world: World
- constructor(bot: Bot, world: World) {
+ constructor (bot: Bot, world: World) {
this.bot = bot
this.sim = new JumpSim(new BotcraftPhysics(bot.registry), world)
this.world = world
}
- private _buildBackupCandidates(bbs: AABB[], goalVert: Vec3, orgPos: Vec3): Vec3[] {
+ private _buildBackupCandidates (bbs: AABB[], goalVert: Vec3, orgPos: Vec3): Vec3[] {
const candidates = new Map()
for (const bb of bbs) {
@@ -340,7 +340,7 @@ export class ParkourJumpHelper {
})
}
- public findGoalVertex(goal: AABB): Vec3 {
+ public findGoalVertex (goal: AABB): Vec3 {
// get top vertex that is closest to target.
const pos = this.bot.entity.position
@@ -388,12 +388,12 @@ export class ParkourJumpHelper {
return minVert
}
- public findBackupVertex(bbs: AABB[], goalVert: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 {
+ public findBackupVertex (bbs: AABB[], goalVert: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 {
const candidates = this._buildBackupCandidates(bbs, goalVert, orgPos)
return candidates[0] ?? orgPos.clone()
}
- public findViableBackupVertex(goal: Vec3, eyeTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 | null {
+ public findViableBackupVertex (goal: Vec3, eyeTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): Vec3 | null {
const bbs = getUnderlyingBBs(this.world, orgPos, 0.6)
const goalVert = eyeTarget ?? this.findGoalVertex(AABB.fromBlockPos(goal))
@@ -420,7 +420,7 @@ export class ParkourJumpHelper {
return result
}
- public simJumpFromEdge(srcBBs: AABB[], goal: Vec3, eyeTarget?: Vec3): boolean {
+ public simJumpFromEdge (srcBBs: AABB[], goal: Vec3, eyeTarget?: Vec3): boolean {
// const bbs = this.getUnderlyingBBs(this.bot.entity.position, 0.6);
// console.log(bbs)
@@ -437,7 +437,7 @@ export class ParkourJumpHelper {
return reached(state, 0) as boolean
}
- public simFallOffEdge(goal: Vec3, target?: Vec3): boolean {
+ public simFallOffEdge (goal: Vec3, target?: Vec3): boolean {
const goalVert = this.findGoalVertex(AABB.fromBlockPos(goal))
// console.log('sim jump goals', goal, goalVert)
@@ -446,7 +446,7 @@ export class ParkourJumpHelper {
const goalBBs = this.world.getBlockInfo(goal).getBBs()
ctx.state.control = ControlStateHandler.DEFAULT()
- if (target) stateLookAt(ctx.state, target)
+ if (target != null) stateLookAt(ctx.state, target)
ctx.state.control.set('forward', true)
ctx.state.control.set('jump', false)
ctx.state.control.set('sprint', true)
@@ -474,14 +474,13 @@ export class ParkourJumpHelper {
return reached0(state, 0) as boolean
}
- public simForwardMove(goal: Vec3, eyeTarget?: Vec3, jump = true, ...constraints: SimulationGoal[]): boolean {
-
+ public simForwardMove (goal: Vec3, eyeTarget?: Vec3, jump = true, ...constraints: SimulationGoal[]): boolean {
// console.log('sim jump goals', goal, goalVert)
const ctx = EPhysicsCtx.FROM_BOT(this.sim.ctx, this.bot)
// const goalCenter = goal.floored().offset(0.5, 0, 0.5)
const goalBBs = this.world.getBlockInfo(goal).getBBs()
-
+
const target = eyeTarget ?? goal
// const orgPos = this.bot.entity.position.clone()
@@ -524,7 +523,7 @@ export class ParkourJumpHelper {
return testwtf
}
- public simBackupJump(goal: Vec3, eyeTarget?: Vec3, backupTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): boolean {
+ public simBackupJump (goal: Vec3, eyeTarget?: Vec3, backupTarget?: Vec3, orgPos: Vec3 = this.bot.entity.position): boolean {
const bbs = getUnderlyingBBs(this.world, orgPos, 0.6)
const goalBBs = this.world.getBlockInfo(goal).getBBs()
diff --git a/src/mineflayer-specific/movements/simulators/jumpSim.ts b/src/mineflayer-specific/movements/simulators/jumpSim.ts
index 62c8ffa..4d38353 100644
--- a/src/mineflayer-specific/movements/simulators/jumpSim.ts
+++ b/src/mineflayer-specific/movements/simulators/jumpSim.ts
@@ -131,7 +131,7 @@ export class JumpSim extends BaseSimulator {
// goalCorner.set(goalBlockTop.x, goalBlockTop.y, goalBlockTop.z)
changed = true
}
-
+
if (ticks > 0 && srcAABBs.every((src) => !src.intersects(playerBB)) && !jump) {
state.control.jump = true
jump = true
diff --git a/src/mineflayer-specific/pathProducers/continuousPathProducer.ts b/src/mineflayer-specific/pathProducers/continuousPathProducer.ts
index a25d3a7..ada0c72 100644
--- a/src/mineflayer-specific/pathProducers/continuousPathProducer.ts
+++ b/src/mineflayer-specific/pathProducers/continuousPathProducer.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { Bot } from 'mineflayer'
import { PathProducer, AStar, AStarNeighbor } from '../../mineflayer-specific/algs'
import * as goals from '../goals'
diff --git a/src/mineflayer-specific/pathProducers/partialPathProducer.ts b/src/mineflayer-specific/pathProducers/partialPathProducer.ts
index 3a5b98b..3a33a80 100644
--- a/src/mineflayer-specific/pathProducers/partialPathProducer.ts
+++ b/src/mineflayer-specific/pathProducers/partialPathProducer.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires */
import { Bot } from 'mineflayer'
import { PathProducer, AStar, AStarNeighbor } from '../../mineflayer-specific/algs'
import * as goals from '../goals'
@@ -139,16 +140,16 @@ export class PartialPathProducer implements PathProducer {
const time1 = performance.now() - this.lastStartTime
const totalTime = performance.now() - this.startTime
-
+
log('Partial Path cost increased by %d to %d. Target Vec: %O', cost, this.latestCost, this.latestMove?.vec)
- log('ITERATION METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)',
- time1.toFixed(2), nodecount, Math.round((nodecount / time1) * 1000),
- seensize, Math.round((seensize / time1) * 1000),
- movecount, Math.round((movecount / time1) * 1000))
- log('TOTAL METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)',
- totalTime.toFixed(2), this.consideredNodeCount, Math.round((this.consideredNodeCount / totalTime) * 1000),
- this.latestClosedNodeCount, Math.round((this.latestClosedNodeCount / totalTime) * 1000),
- this.latestMoveCount, Math.round((this.latestMoveCount / totalTime) * 1000))
+ log('ITERATION METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)',
+ time1.toFixed(2), nodecount, Math.round((nodecount / time1) * 1000),
+ seensize, Math.round((seensize / time1) * 1000),
+ movecount, Math.round((movecount / time1) * 1000))
+ log('TOTAL METRICS | Time: %dms | Nodes: %d (%d n/s) | Seen: %d (%d s/s) | Moves: %d (%d m/s)',
+ totalTime.toFixed(2), this.consideredNodeCount, Math.round((this.consideredNodeCount / totalTime) * 1000),
+ this.latestClosedNodeCount, Math.round((this.latestClosedNodeCount / totalTime) * 1000),
+ this.latestMoveCount, Math.round((this.latestMoveCount / totalTime) * 1000))
this.lastStartTime = performance.now()
} else {
diff --git a/src/mineflayer-specific/post/index.ts b/src/mineflayer-specific/post/index.ts
index 702d52c..d8876c4 100644
--- a/src/mineflayer-specific/post/index.ts
+++ b/src/mineflayer-specific/post/index.ts
@@ -9,6 +9,5 @@ export * from './registry'
export type BuildableMoveOptimizer = new (bot: Bot, world: World, settings: Partial) => MovementOptimizer
-
export type OptimizationSetup = Map
export type ReplacementMap = Map
diff --git a/src/mineflayer-specific/post/optimizer.ts b/src/mineflayer-specific/post/optimizer.ts
index 8966332..83b6f7c 100644
--- a/src/mineflayer-specific/post/optimizer.ts
+++ b/src/mineflayer-specific/post/optimizer.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions */
import { Bot } from 'mineflayer'
import type { OptimizationMap } from '.'
import type { BuildableMoveProvider } from '../movements'
@@ -30,7 +31,7 @@ export abstract class MovementOptimizer {
*
* Default behavior keeps the start move's provider so existing optimizer/executor
* mappings continue to work unchanged.
- *
+ *
* For now, changing this here does not work. Do not use this to set a different movement provider.
*/
protected getMergedMoveType (startIndex: number, endIndex: number, path: readonly Move[]): MovementProvider {
@@ -115,7 +116,6 @@ export class Optimizer {
return !!this.pathCopy
}
-
async compute (): Promise {
if (!this.sanitize()) {
throw new Error('Optimizer not sanitized')
@@ -163,7 +163,7 @@ export class Optimizer {
this.currentIndex++
}
- log(`compute() finished. Final optimized path length: ${this.pathCopy.length}. End: ${this.pathCopy[this.pathCopy.length -1 ].exitPos}`)
+ log(`compute() finished. Final optimized path length: ${this.pathCopy.length}. End: ${this.pathCopy[this.pathCopy.length - 1].exitPos}`)
return this.pathCopy
}
diff --git a/src/mineflayer-specific/post/optimizers.ts b/src/mineflayer-specific/post/optimizers.ts
index 799701f..253d8ef 100644
--- a/src/mineflayer-specific/post/optimizers.ts
+++ b/src/mineflayer-specific/post/optimizers.ts
@@ -1,8 +1,14 @@
+/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions */
import { ControlStateHandler, EPhysicsCtx } from '@nxg-org/mineflayer-physics-util'
+import { Vec3 } from 'vec3'
import { Move } from '../move'
import type { RayType } from '../../types'
import { BlockInfo } from '../world/cacheWorld'
import { MovementOptimizer } from './optimizer'
+import { World } from '../world/worldInterface'
+import { sumExclusionAreas } from '../movements/movement'
+import type { ExclusionArea } from '../movements/exclusionZones'
+import { COST_INF } from '../movements/costs'
import { AABB, AABBUtils } from '@nxg-org/mineflayer-util-plugin'
import { stateLookAt } from '../movements/movementUtils'
@@ -10,10 +16,69 @@ import { stateLookAt } from '../movements/movementUtils'
const debug = require('debug')
const log = debug('minecraft-pathfinding:optimizers')
+/**
+ * Walk the straight segment from `from` to `to` with a voxel traversal
+ * (Amanatides & Woo) and return true as soon as a cell lands inside a HARD step
+ * zone (summed weight >= COST_INF). Visiting exactly the cells the segment
+ * crosses keeps the check correct (no skipped cells) and cheap.
+ *
+ * Only hard zones stop a straight-line merge; soft zones are a preference, not a
+ * wall. Returns false immediately when there are no step areas.
+ */
+export function lineCrossesHardExclusion (world: World, from: Vec3, to: Vec3, areas: ExclusionArea[]): boolean {
+ if (areas.length === 0) return false
+
+ let x = Math.floor(from.x)
+ let y = Math.floor(from.y)
+ let z = Math.floor(from.z)
+ const endX = Math.floor(to.x)
+ const endY = Math.floor(to.y)
+ const endZ = Math.floor(to.z)
+
+ const dx = to.x - from.x
+ const dy = to.y - from.y
+ const dz = to.z - from.z
+
+ const stepX = Math.sign(dx)
+ const stepY = Math.sign(dy)
+ const stepZ = Math.sign(dz)
+
+ // The segment is parameterised by t in [0, 1]. tMax* is the t at which we next
+ // cross a cell boundary on that axis; tDelta* is the t to cross one whole cell.
+ const tDeltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity
+ const tDeltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity
+ const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity
+
+ let tMaxX = stepX !== 0 ? (stepX > 0 ? x + 1 - from.x : from.x - x) / Math.abs(dx) : Infinity
+ let tMaxY = stepY !== 0 ? (stepY > 0 ? y + 1 - from.y : from.y - y) / Math.abs(dy) : Infinity
+ let tMaxZ = stepZ !== 0 ? (stepZ > 0 ? z + 1 - from.z : from.z - z) / Math.abs(dz) : Infinity
+
+ // Cells to visit = Manhattan distance in cells + 1. A fixed loop count (rather
+ // than tMax comparisons) keeps termination floating-point safe.
+ const cells = Math.abs(endX - x) + Math.abs(endY - y) + Math.abs(endZ - z)
+
+ for (let i = 0; i <= cells; i++) {
+ if (sumExclusionAreas(areas, world.getBlockInfo(new Vec3(x, y, z))) >= COST_INF) return true
+
+ if (tMaxX <= tMaxY && tMaxX <= tMaxZ) {
+ x += stepX
+ tMaxX += tDeltaX
+ } else if (tMaxY <= tMaxZ) {
+ y += stepY
+ tMaxY += tDeltaY
+ } else {
+ z += stepZ
+ tMaxZ += tDeltaZ
+ }
+ }
+ return false
+}
+
export class LandStraightAheadOpt extends MovementOptimizer {
async identEndOpt (currentIndex: number, path: Move[]): Promise {
const startIndex = currentIndex
const thisMove = path[currentIndex] // starting move
+ const stepAreas = thisMove.moveType.settings.exclusionAreasStep
let lastMove = path[currentIndex]
let nextMove = path[++currentIndex]
@@ -21,7 +86,7 @@ export class LandStraightAheadOpt extends MovementOptimizer {
log(`[LandStraightAhead] Optimizing from index ${startIndex} (${thisMove.moveType.constructor.name})`)
if (nextMove === undefined) {
- log(`[LandStraightAhead] nextMove is undefined, aborting.`)
+ log('[LandStraightAhead] nextMove is undefined, aborting.')
return --currentIndex
}
@@ -51,20 +116,20 @@ export class LandStraightAheadOpt extends MovementOptimizer {
log(`[LandStraightAhead] Index ${currentIndex}: nextMove became undefined.`)
return --currentIndex
}
-
+
for (const vert of verts) {
const offset = vert.minus(orgPos)
const test1 = nextMove.exitPos.offset(0, orgY - nextMove.exitPos.y, 0)
const test = test1.plus(offset)
const dist = nextMove.exitPos.distanceTo(orgPos)
-
+
const raycast0 = this.bot.world.raycast(
vert,
test.minus(vert).normalize(),
dist,
(block) => (!BlockInfo.replaceables.has(block.type) || BlockInfo.liquids.has(block.type) || BlockInfo.blocksToAvoid.has(block.type)) && block.shapes.length > 0
) as unknown as RayType | null
-
+
const valid0 = (raycast0 == null) || raycast0.position.distanceTo(orgPos) > dist
if (!valid0) {
@@ -79,7 +144,7 @@ export class LandStraightAheadOpt extends MovementOptimizer {
const test1 = nextMove.exitPos.offset(0, orgY - nextMove.exitPos.y, 0)
const test = test1.plus(offset)
const dist = nextMove.exitPos.distanceTo(orgPos)
-
+
const raycast0 = (await this.bot.world.raycast(
vert,
test.minus(vert).normalize(),
@@ -100,14 +165,21 @@ export class LandStraightAheadOpt extends MovementOptimizer {
return --currentIndex
}
+ // Exclusion zones: do not straight-line the merge through a hard "keep out"
+ // area the original route went around. Stop before this move if it would.
+ if (lineCrossesHardExclusion(this.world, orgPos, nextMove.exitPos, stepAreas)) {
+ log(`[LandStraightAhead] Index ${currentIndex}: straight line would cross a hard exclusion zone.`)
+ return --currentIndex
+ }
+
if (++currentIndex >= path.length) {
- log(`[LandStraightAhead] Reached end of path.`)
+ log('[LandStraightAhead] Reached end of path.')
return --currentIndex
}
lastMove = nextMove
nextMove = path[currentIndex]
}
-
+
log(`[LandStraightAhead] Y-level changed or loop ended naturally. Returning index ${currentIndex - 1}.`)
return --currentIndex
}
@@ -165,13 +237,13 @@ export class DropDownOpt extends MovementOptimizer {
const blockBB1 = AABB.fromBlockPos(nextMove.exitPos.offset(0, -1, 0))
let flag = false
let good = false
-
+
this.sim.simulateUntil(
(state, ticks) => {
const pBB = AABBUtils.getPlayerAABB({ position: ctx.state.pos, width: 0.6, height: 1.8 })
const collided =
(pBB.collides(blockBB0) && bb0solid) || (pBB.collides(blockBB1) && bb1solid && (state.onGround || state.isInWater))
-
+
if (collided) {
good = true
return true
@@ -208,8 +280,7 @@ export class DropDownOpt extends MovementOptimizer {
if (flag0) {
log(`[DropDownOpt] Index ${currentIndex}: Flag0 triggered. Returning.`)
return currentIndex
- }
- else flag0 = true
+ } else flag0 = true
}
if (++currentIndex >= path.length) return --currentIndex
@@ -230,7 +301,7 @@ export class ForwardJumpUpOpt extends MovementOptimizer {
log(`[ForwardJumpUpOpt] Optimizing from index ${startIndex} (${lastMove.moveType.constructor.name})`)
if (lastMove.toPlace.length > 0) {
- log(`[ForwardJumpUpOpt] Initial move places a block. Aborting.`)
+ log('[ForwardJumpUpOpt] Initial move places a block. Aborting.')
return --currentIndex
}
@@ -251,7 +322,7 @@ export class ForwardJumpUpOpt extends MovementOptimizer {
log(`[ForwardJumpUpOpt] Index ${currentIndex}: AABB collision failed.`)
return --currentIndex
}
-
+
if (++currentIndex >= path.length) return --currentIndex
lastMove = nextMove
nextMove = path[currentIndex]
@@ -261,7 +332,7 @@ export class ForwardJumpUpOpt extends MovementOptimizer {
while (
lastMove.exitPos.y === nextMove.exitPos.y &&
- nextMove.exitPos.distanceTo(firstPos) <= 2 &&
+ nextMove.exitPos.distanceTo(firstPos) <= 2 &&
nextMove.toPlace.length === 0 &&
nextMove.toBreak.length === 0
) {
diff --git a/src/mineflayer-specific/post/registry.ts b/src/mineflayer-specific/post/registry.ts
index 785086c..00f41a3 100644
--- a/src/mineflayer-specific/post/registry.ts
+++ b/src/mineflayer-specific/post/registry.ts
@@ -6,7 +6,7 @@ import { MovementOptimizer } from './optimizer'
export type BuildableMoveOptimizer = new (bot: Bot, world: World, settings: Partial) => MovementOptimizer
-export type RegisteredOptimizer = {
+export interface RegisteredOptimizer {
optimizer: MovementOptimizer
optimizedExecutor?: MovementExecutor
priority: number
diff --git a/src/mineflayer-specific/post/replacement.ts b/src/mineflayer-specific/post/replacement.ts
index 2d91f25..d532515 100644
--- a/src/mineflayer-specific/post/replacement.ts
+++ b/src/mineflayer-specific/post/replacement.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
import { Move } from '../move'
import { Algorithm, Path } from '../../abstract'
import type { ReplacementMap } from '.'
diff --git a/src/mineflayer-specific/post/replacements.ts b/src/mineflayer-specific/post/replacements.ts
index 6e65262..b5a8c0c 100644
--- a/src/mineflayer-specific/post/replacements.ts
+++ b/src/mineflayer-specific/post/replacements.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { Vec3 } from 'vec3'
import { Path, MovementProvider as AMovementProvider, PathNode, Algorithm } from '../../abstract'
import { Move } from '../move'
diff --git a/src/mineflayer-specific/world/cacheWorld.ts b/src/mineflayer-specific/world/cacheWorld.ts
index f1074d5..9cff817 100644
--- a/src/mineflayer-specific/world/cacheWorld.ts
+++ b/src/mineflayer-specific/world/cacheWorld.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-non-null-assertion */
import { Vec3 } from 'vec3'
import type { World as WorldType } from './worldInterface'
import { Bot } from 'mineflayer'
@@ -91,7 +92,7 @@ export class BlockInfo {
}
BlockInfo.PBlock = pBlock(registry) // require('prismarine-block')(registry)
- BlockInfo.substituteBlockStateId = registry.blocksByName.dirt.minStateId;
+ BlockInfo.substituteBlockStateId = registry.blocksByName.dirt.minStateId
BlockInfo._waterBlock = BlockInfo.PBlock.fromStateId(registry.blocksByName.water.minStateId, 0)
BlockInfo._waterBlock.position = new Vec3(0, 0, 0)
@@ -554,7 +555,7 @@ export class CacheSyncWorld implements WorldType {
const calls = this.cacheCalls
this.cacheCalls = 0
// const used = Object.keys(this.posCache).length === 0 ? this.blocks : this.posCache
- const used = this.blockInfos
+ const used = this.blockInfos
return `size = ${used.size}; calls = ${calls}`
}
diff --git a/src/mineflayer-specific/world/utils.ts b/src/mineflayer-specific/world/utils.ts
index 5313c27..0c9af21 100644
--- a/src/mineflayer-specific/world/utils.ts
+++ b/src/mineflayer-specific/world/utils.ts
@@ -13,7 +13,7 @@ export function fasterGetBlock (this: Bot['world'], pos: Vec3): Block {
return null as unknown as Block
}
- const colPos = new Vec3(pos.x & 0xf, pos.y, pos.z & 0xf).floor();
+ const colPos = new Vec3(pos.x & 0xf, pos.y, pos.z & 0xf).floor()
const ret1 = col.getBlock(colPos)
ret1.position = pos
diff --git a/src/pathExecutor.ts b/src/pathExecutor.ts
index a82fda6..09b4401 100644
--- a/src/pathExecutor.ts
+++ b/src/pathExecutor.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/restrict-template-expressions, no-unmodified-loop-condition, @typescript-eslint/no-throw-literal, @typescript-eslint/strict-boolean-expressions */
import { Bot } from 'mineflayer'
import { Vec3 } from 'vec3'
import { Path } from './mineflayer-specific/algs'
@@ -7,7 +8,7 @@ import { AbortError, CancelError, ManualResetError, ResetError } from './minefla
import type {
BuildableMoveProvider,
ExecutorMap,
- MovementExecutor,
+ MovementExecutor
} from './mineflayer-specific/movements'
import type { OptimizationMap } from './mineflayer-specific/post'
import { Optimizer } from './mineflayer-specific/post'
@@ -28,18 +29,18 @@ export interface ExecutionMappings {
}
export interface PathfinderExecutionHost {
- getBot(): Bot
- getWorld(): World
- getActiveMappings(): ExecutionMappings
- cleanupBot(): Promise
- getPathFromToRaw(startPos: Vec3, startVel: Vec3, goal: goals.Goal): Promise
+ getBot: () => Bot
+ getWorld: () => World
+ getActiveMappings: () => ExecutionMappings
+ cleanupBot: () => Promise
+ getPathFromToRaw: (startPos: Vec3, startVel: Vec3, goal: goals.Goal) => Promise
}
type RunnerStage = 'idle' | 'align' | 'optimize' | 'init' | 'perform'
const MAX_TASK_TIME_MS = 40
-function isNodeRuntime(): boolean {
+function isNodeRuntime (): boolean {
return typeof process !== 'undefined' && process.versions?.bun == null
}
@@ -51,58 +52,58 @@ export class PathExecutor {
private currentExecutor?: MovementExecutor
private resetReason?: ResetReason
- public constructor(private readonly bot: Bot, private readonly host: ThePathfinder) { }
+ public constructor (private readonly bot: Bot, private readonly host: ThePathfinder) { }
- public bumpExecutionId(): number {
+ public bumpExecutionId (): number {
this.currentExecutionId++
return this.currentExecutionId
}
- public getCurrentExecutionId(): number {
+ public getCurrentExecutionId (): number {
return this.currentExecutionId
}
- public getCurrentIndex(): number {
+ public getCurrentIndex (): number {
return this.currentIndex
}
- public setCurrentIndex(index: number): void {
+ public setCurrentIndex (index: number): void {
this.currentIndex = index
}
- public getCurrentPath(): Move[] | undefined {
+ public getCurrentPath (): Move[] | undefined {
return this.currentPath
}
- public setCurrentPath(path?: Move[]): void {
+ public setCurrentPath (path?: Move[]): void {
this.currentPath = path
}
- public getCurrentMove(): Move | undefined {
+ public getCurrentMove (): Move | undefined {
return this.currentMove
}
- public setCurrentMove(move?: Move): void {
+ public setCurrentMove (move?: Move): void {
this.currentMove = move
}
- public getCurrentExecutor(): MovementExecutor | undefined {
+ public getCurrentExecutor (): MovementExecutor | undefined {
return this.currentExecutor
}
- public setCurrentExecutor(executor?: MovementExecutor): void {
+ public setCurrentExecutor (executor?: MovementExecutor): void {
this.currentExecutor = executor
}
- public getResetReason(): ResetReason | undefined {
+ public getResetReason (): ResetReason | undefined {
return this.resetReason
}
- public setResetReason(reason?: ResetReason): void {
+ public setResetReason (reason?: ResetReason): void {
this.resetReason = reason
}
- public clearResetReason(): void {
+ public clearResetReason (): void {
delete this.resetReason
}
@@ -158,7 +159,7 @@ export class PathExecutor {
return value as T
}
- private findNextCurrentIdx(move: Move, localPath: Move[], currentIndex: number, adding?: boolean | number): number {
+ private findNextCurrentIdx (move: Move, localPath: Move[], currentIndex: number, adding?: boolean | number): number {
const endIdx = localPath.findIndex(
(m, i) => i >= currentIndex && m.exitPos.distanceTo(move.exitPos) < 0.1
)
@@ -172,14 +173,14 @@ export class PathExecutor {
return currentIndex
}
- public async perform(path: Path, goal: goals.Goal, entry = 0): Promise {
+ public async perform (path: Path, goal: goals.Goal, entry = 0): Promise {
const MAX_RECOVERY_DEPTH = 0
if (entry > MAX_RECOVERY_DEPTH) {
throw new Error('Too many failures, exiting performing.')
}
- const bot = this.bot;
+ const bot = this.bot
const myExecutionId = this.bumpExecutionId()
const localPath = path.path
let currentIndex = 0
@@ -203,7 +204,6 @@ export class PathExecutor {
let nonNodeDrainInFlight = false
-
const completion = new Promise((resolve, reject) => {
resolveCompletion = resolve
rejectCompletion = reject
@@ -218,7 +218,7 @@ export class PathExecutor {
settled = true
bot.off('physicsTickBegin', moveListener)
if (this.getCurrentExecutionId() === myExecutionId) {
- await this.timeAsync('finish cleanup', () => this.host.cleanupBot())
+ await this.timeAsync('finish cleanup', async () => await this.host.cleanupBot())
}
resolveCompletion()
}
@@ -245,7 +245,6 @@ export class PathExecutor {
}
const handleExecutionError = async (err: unknown): Promise => {
-
log(`Error: ${err}`)
if (err instanceof AbortError) {
@@ -296,7 +295,7 @@ export class PathExecutor {
const optimizer = new Optimizer(bot, this.host.world, optimizers)
optimizer.loadPath(localPath.slice(currentIndex))
- pendingOptimize = this.timeAsync('optimizer.compute', () => optimizer.compute()).then(
+ pendingOptimize = this.timeAsync('optimizer.compute', async () => await optimizer.compute()).then(
(result) => {
if (settled) return
if (this.getCurrentExecutionId() !== myExecutionId) return
@@ -336,8 +335,6 @@ export class PathExecutor {
throw new Error('No executor for movement type ' + move.moveType.constructor.name)
}
-
-
this.setCurrentMove(move)
this.setCurrentExecutor(executor)
this.setCurrentIndex(currentIndex)
@@ -346,7 +343,7 @@ export class PathExecutor {
log(`[ExecID %d] Entering movement ${move.moveType.constructor.name}. Idx: ${this.getCurrentIndex()}, start: ${move.entryPos}, end: ${move.exitPos}`, myExecutionId)
tickCount = 0
- await this.timeAsync('prepare cleanup', () => this.host.cleanupBot())
+ await this.timeAsync('prepare cleanup', async () => await this.host.cleanupBot())
executor.loadMove(move)
if (executor.isAlreadyCompleted(move, tickCount, goal)) {
@@ -405,7 +402,7 @@ export class PathExecutor {
const aligned = await this.timeAsync(
`align ${move.moveType.constructor.name}`,
- () => executor.align(move, tickCount, goal)
+ async () => await executor.align(move, tickCount, goal)
)
tickCount++
@@ -413,7 +410,7 @@ export class PathExecutor {
runnerStage = 'init'
pendingInit = this.timeAsync(
`performInit ${move.moveType.constructor.name}`,
- () => executor._performInit(move, currentIndex, localPath)
+ async () => await executor._performInit(move, currentIndex, localPath)
)
pendingInit.then(
() => {
@@ -445,7 +442,7 @@ export class PathExecutor {
const adding = await this.timeAsync(
`performPerTick ${move.moveType.constructor.name}`,
- () => executor._performPerTick(move, tickCount, currentIndex, localPath)
+ async () => await executor._performPerTick(move, tickCount, currentIndex, localPath)
)
tickCount++
@@ -463,7 +460,6 @@ export class PathExecutor {
}
}
} catch (err) {
-
log(`Error: ${err}`)
if (err instanceof AbortError) {
executorSafeReset(this.getCurrentExecutor())
@@ -507,7 +503,7 @@ export class PathExecutor {
}
}
- let tickInFlight = false
+ const tickInFlight = false
const moveListener = (): void => {
if (settled || tickInFlight || recoveryInFlight) return
@@ -548,8 +544,8 @@ export class PathExecutor {
}
}
- public async recovery(move: Move, path: Path, goal: goals.Goal, entry = 0): Promise {
- const bot = this.bot;
+ public async recovery (move: Move, path: Path, goal: goals.Goal, entry = 0): Promise {
+ const bot = this.bot
log(`recovery ${entry} for ${move.moveType.constructor.name}`)
while (!bot.entity.onGround && !(bot.entity as any).isInWater) {
@@ -557,7 +553,7 @@ export class PathExecutor {
}
bot.emit('enteredRecovery', entry)
- await this.timeAsync('recovery cleanup', () => this.host.cleanupBot())
+ await this.timeAsync('recovery cleanup', async () => await this.host.cleanupBot())
const ind = path.path.findIndex((m) => m.entryPos.distanceTo(move.entryPos) < 0.1)
if (ind === -1) {
@@ -585,7 +581,7 @@ export class PathExecutor {
const path1 = await this.timeAsync(
'recovery pathfinding',
- () => this.host.getPathFromToRaw(bot.entity.position, EMPTY_VEC, newGoal)
+ async () => await this.host.getPathFromToRaw(bot.entity.position, EMPTY_VEC, newGoal)
)
if (path1 === null) {
@@ -608,7 +604,7 @@ export class PathExecutor {
}
}
- private check(): void {
+ private check (): void {
const resetReason = this.getResetReason()
if (resetReason != null) {
throw new ResetError(resetReason)
diff --git a/src/utils.ts b/src/utils.ts
index b89beb8..192e4df 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,11 +1,12 @@
+/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { Bot, BotEvents } from 'mineflayer'
import { Vec3 } from 'vec3'
import { BlockInfo } from './mineflayer-specific/world/cacheWorld'
-import { BlockFace } from '@nxg-org/mineflayer-util-plugin'
-import { AABBUtils } from '@nxg-org/mineflayer-util-plugin'
+import { BlockFace, AABBUtils } from '@nxg-org/mineflayer-util-plugin'
+
import { World } from './mineflayer-specific/world/worldInterface'
-export function printBotControls(bot: Bot, log: (...args: unknown[]) => void = console.log): void {
+export function printBotControls (bot: Bot, log: (...args: unknown[]) => void = console.log): void {
const controls = {
forward: bot.getControlState('forward'),
back: bot.getControlState('back'),
@@ -13,7 +14,7 @@ export function printBotControls(bot: Bot, log: (...args: unknown[]) => void = c
right: bot.getControlState('right'),
jump: bot.getControlState('jump'),
sprint: bot.getControlState('sprint'),
- sneak: bot.getControlState('sneak'),
+ sneak: bot.getControlState('sneak')
}
log(
@@ -28,19 +29,19 @@ export function printBotControls(bot: Bot, log: (...args: unknown[]) => void = c
)
}
-export function faceToVec(face: BlockFace): Vec3 {
- switch (face) {
- case BlockFace.BOTTOM: return new Vec3(0, -1, 0)
- case BlockFace.TOP: return new Vec3(0, 1, 0)
- case BlockFace.NORTH: return new Vec3(0, 0, -1)
- case BlockFace.SOUTH: return new Vec3(0, 0, 1)
- case BlockFace.WEST: return new Vec3(-1, 0, 0)
- case BlockFace.EAST: return new Vec3(1, 0, 0)
- default: throw new Error('Invalid face')
- }
+export function faceToVec (face: BlockFace): Vec3 {
+ switch (face) {
+ case BlockFace.BOTTOM: return new Vec3(0, -1, 0)
+ case BlockFace.TOP: return new Vec3(0, 1, 0)
+ case BlockFace.NORTH: return new Vec3(0, 0, -1)
+ case BlockFace.SOUTH: return new Vec3(0, 0, 1)
+ case BlockFace.WEST: return new Vec3(-1, 0, 0)
+ case BlockFace.EAST: return new Vec3(1, 0, 0)
+ default: throw new Error('Invalid face')
}
+}
-export function *interpolateStepPoints(start: Vec3, end: Vec3, step = 0.8): Generator {
+export function * interpolateStepPoints (start: Vec3, end: Vec3, step = 0.8): Generator {
const delta = end.minus(start)
const dist = delta.norm()
@@ -61,7 +62,6 @@ export function *interpolateStepPoints(start: Vec3, end: Vec3, step = 0.8): Gene
}
}
-
export const debug = (bot: Bot | undefined, ...args: any[]): void => {
if (bot != null) {
bot.chat(args.join(' '))
@@ -118,7 +118,7 @@ export function getNormalizedPos (bot: Bot, startPos?: Vec3): Vec3 {
return pos
}
-export function getSupportedStartPos(world: World, startPos: Vec3): Vec3 {
+export function getSupportedStartPos (world: World, startPos: Vec3): Vec3 {
if (!BlockInfo.initialized) throw new Error('BlockInfo not initialized')
const pos = startPos.clone()
diff --git a/tests/exclusionZones.test.ts b/tests/exclusionZones.test.ts
new file mode 100644
index 0000000..cc6462b
--- /dev/null
+++ b/tests/exclusionZones.test.ts
@@ -0,0 +1,210 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import { Vec3 } from 'vec3'
+
+import { createPlugin, goals, Move } from '../src'
+import type { ExclusionArea } from '../src'
+import { buildMovementOptions, DEFAULT_MOVEMENT_OPTS } from '../src/mineflayer-specific/movements'
+import { lineCrossesHardExclusion } from '../src/mineflayer-specific/post/optimizers'
+import { createCacheWorld } from './setup'
+
+// ---------------------------------------------------------------------------
+// Local helpers (kept here so this file stands on its own). The library ships
+// no zone-builders on purpose, so the tests define their own — just like a user
+// would (see examples/exclusionZones.js).
+// ---------------------------------------------------------------------------
+
+type PathResult = {
+ status: string
+ path: Move[]
+}
+
+/** A hard/soft box zone between two corners (inclusive). */
+function boxExclusion (min: Vec3, max: Vec3, cost = Infinity): ExclusionArea {
+ return (block) => {
+ const p = block.position
+ const inside =
+ p.x >= min.x && p.x <= max.x &&
+ p.y >= min.y && p.y <= max.y &&
+ p.z >= min.z && p.z <= max.z
+ return inside ? cost : 0
+ }
+}
+
+function withTimeout (promise: Promise, ms: number, message: string): Promise {
+ let timer: NodeJS.Timeout | undefined
+ return Promise.race([
+ promise.finally(() => {
+ if (timer != null) clearTimeout(timer)
+ }),
+ new Promise((_, reject) => {
+ timer = setTimeout(() => reject(new Error(message)), ms)
+ })
+ ])
+}
+
+function preparePathRig (moveSettings?: Record) {
+ const rig = createCacheWorld('1.20.4', 64, new Vec3(0, 64, 0)).rig
+ rig.bot.loadPlugin(createPlugin({
+ pathfinderSettings: { partialPathProducer: true, partialPathLength: 50 },
+ moveSettings: moveSettings as any
+ }))
+ return rig
+}
+
+async function collectPathResult (
+ bot: ReturnType['bot'],
+ goal: goals.Goal,
+ timeoutMs = 15000
+): Promise {
+ let final: { result: PathResult } | undefined
+
+ await withTimeout((async () => {
+ for await (const res of bot.pathfinder.getPathTo(goal)) {
+ final = res as { result: PathResult }
+ }
+ })(), timeoutMs, `timed out while planning to ${goal.constructor.name}`)
+
+ if (final == null) {
+ throw new Error('path planner finished without a final result')
+ }
+
+ return final.result
+}
+
+// ---------------------------------------------------------------------------
+// Default-array safety: the three exclusion lists must never be shared/mutated.
+// Without fresh copies, mutating one bot's defaultMoveSettings.exclusionAreasStep
+// would leak zones into other bots and into later setMoveOptions calls.
+// ---------------------------------------------------------------------------
+
+test('buildMovementOptions gives every settings object its own exclusion arrays', () => {
+ const a = buildMovementOptions()
+ const b = buildMovementOptions()
+
+ assert.notEqual(a.exclusionAreasStep, DEFAULT_MOVEMENT_OPTS.exclusionAreasStep)
+ assert.notEqual(a.exclusionAreasStep, b.exclusionAreasStep)
+
+ a.exclusionAreasStep.push(() => 0)
+ assert.equal(a.exclusionAreasStep.length, 1)
+ assert.equal(b.exclusionAreasStep.length, 0)
+ assert.equal(DEFAULT_MOVEMENT_OPTS.exclusionAreasStep.length, 0)
+})
+
+test('buildMovementOptions copies a user-supplied array instead of holding its reference', () => {
+ const mine: ExclusionArea[] = []
+ const opts = buildMovementOptions({ exclusionAreasBreak: mine })
+
+ assert.notEqual(opts.exclusionAreasBreak, mine)
+ mine.push(() => 0)
+ assert.equal(opts.exclusionAreasBreak.length, 0)
+})
+
+test('the default exclusion arrays are frozen so they cannot be mutated in place', () => {
+ assert.ok(Object.isFrozen(DEFAULT_MOVEMENT_OPTS.exclusionAreasStep))
+ assert.ok(Object.isFrozen(DEFAULT_MOVEMENT_OPTS.exclusionAreasBreak))
+ assert.ok(Object.isFrozen(DEFAULT_MOVEMENT_OPTS.exclusionAreasPlace))
+})
+
+// ---------------------------------------------------------------------------
+// Functional tests: run the real pathfinder over a flat world.
+// ---------------------------------------------------------------------------
+
+// A forbidden "wall" straddling the straight route from (0,64,0) to (20,64,0).
+// The bot stands at y=64, so y 64-66 covers feet+head.
+function makeWall (): { area: ExclusionArea, isInside: (x: number, y: number, z: number) => boolean } {
+ const minX = 8; const maxX = 12
+ const minZ = -3; const maxZ = 3
+ const minY = 64; const maxY = 66
+ return {
+ area: boxExclusion(new Vec3(minX, minY, minZ), new Vec3(maxX, maxY, maxZ)),
+ isInside: (x, y, z) => x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ
+ }
+}
+
+test('a hard step exclusion forces the bot to detour around a wall', async () => {
+ const wall = makeWall()
+ const rig = preparePathRig({ exclusionAreasStep: [wall.area] })
+
+ try {
+ const result = await collectPathResult(rig.bot, new goals.GoalBlock(20, 64, 0))
+ const last = result.path[result.path.length - 1]
+
+ assert.equal(result.status, 'success')
+ assert.equal(last?.x, 20)
+ assert.equal(last?.y, 64)
+ assert.equal(last?.z, 0)
+
+ for (const node of result.path) {
+ assert.ok(
+ !wall.isInside(node.x, node.y, node.z),
+ `path stepped into the excluded wall at ${node.x},${node.y},${node.z}`
+ )
+ }
+
+ assert.ok(result.path.length > 21, `expected a detour longer than 21 moves, got ${result.path.length}`)
+ } finally {
+ rig.stopPassivePhysics()
+ }
+})
+
+test('a soft step exclusion still reaches the goal (avoid, never forbid)', async () => {
+ const softWall = boxExclusion(new Vec3(8, 64, -3), new Vec3(12, 66, 3), 40)
+ const rig = preparePathRig({ exclusionAreasStep: [softWall] })
+
+ try {
+ const result = await collectPathResult(rig.bot, new goals.GoalBlock(20, 64, 0))
+ const last = result.path[result.path.length - 1]
+
+ assert.equal(result.status, 'success')
+ assert.equal(last?.x, 20)
+ assert.equal(last?.y, 64)
+ assert.equal(last?.z, 0)
+ } finally {
+ rig.stopPassivePhysics()
+ }
+})
+
+test('a hard step exclusion on the only goal block makes the goal unreachable', async () => {
+ const onGoal = boxExclusion(new Vec3(20, 64, 0), new Vec3(20, 64, 0))
+ const rig = preparePathRig({ exclusionAreasStep: [onGoal] })
+
+ try {
+ await assert.rejects(
+ collectPathResult(rig.bot, new goals.GoalBlock(20, 64, 0), 4000),
+ /timed out while planning to GoalBlock/
+ )
+ } finally {
+ rig.stopPassivePhysics()
+ }
+})
+
+// ---------------------------------------------------------------------------
+// Post-processing: the optimizers must not straight-line a path through a hard
+// zone. That check lives in LandStraightAheadOpt via lineCrossesHardExclusion;
+// these test the voxel-traversal helper directly and deterministically.
+// ---------------------------------------------------------------------------
+
+// getBlockInfo only needs to echo the position back; the zone functions look at
+// block.position.
+const fakeWorld: any = { getBlockInfo: (pos: Vec3) => ({ position: pos }) }
+
+test('lineCrossesHardExclusion flags a hard cell on a straight segment', () => {
+ const wall = boxExclusion(new Vec3(5, 64, 0), new Vec3(5, 64, 0)) // single hard cell
+ // (0.5,64,0.5) -> (10.5,64,0.5) passes through cell (5,64,0).
+ assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(10.5, 64, 0.5), [wall]), true)
+ // A parallel line at z=3 never enters the cell.
+ assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 3.5), new Vec3(10.5, 64, 3.5), [wall]), false)
+})
+
+test('lineCrossesHardExclusion walks diagonals exactly (no skipped cells)', () => {
+ const cell = boxExclusion(new Vec3(3, 64, 3), new Vec3(3, 64, 3))
+ // (0.5,64,0.5) -> (6.5,64,6.5) crosses (3,64,3) on the diagonal.
+ assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(6.5, 64, 6.5), [cell]), true)
+})
+
+test('lineCrossesHardExclusion ignores soft zones and empty lists', () => {
+ const soft = boxExclusion(new Vec3(5, 64, 0), new Vec3(5, 64, 0), 50)
+ assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(10.5, 64, 0.5), [soft]), false)
+ assert.equal(lineCrossesHardExclusion(fakeWorld, new Vec3(0.5, 64, 0.5), new Vec3(10.5, 64, 0.5), []), false)
+})