feat: Refactor mouse controls, fixing all false entity/item interaction issues (#286)

This commit is contained in:
Vitaly 2025-02-27 15:26:38 +03:00 committed by GitHub
commit ceb4cb0b66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 1066 additions and 594 deletions

View file

@ -145,6 +145,7 @@
"http-browserify": "^1.7.0",
"http-server": "^14.1.1",
"https-browserify": "^1.0.0",
"mineflayer-mouse": "^0.0.4",
"mc-assets": "^0.2.37",
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
"mineflayer": "github:zardoy/mineflayer",

858
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -3,20 +3,10 @@ import { activeModalStack, isGameActive, miscUiState, showModal } from './global
import { options } from './optionsStorage'
import { hideNotification, notificationProxy } from './react/NotificationProvider'
import { pointerLock } from './utils'
import worldInteractions from './worldInteractions'
import { updateMotion, initMotionTracking } from './react/uiMotion'
let lastMouseMove: number
const MOTION_DAMPING = 0.92
const MAX_MOTION_OFFSET = 30
const motionVelocity = { x: 0, y: 0 }
const lastUpdate = performance.now()
export const updateCursor = () => {
worldInteractions.update()
}
export type CameraMoveEvent = {
movementX: number
movementY: number
@ -30,7 +20,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
e.stopPropagation?.()
const now = performance.now()
// todo: limit camera movement for now to avoid unexpected jumps
if (now - lastMouseMove < 4) return
if (now - lastMouseMove < 4 && !options.preciseMouseInput) return
lastMouseMove = now
let { mouseSensX, mouseSensY } = options
if (mouseSensY === -1) mouseSensY = mouseSensX
@ -38,7 +28,7 @@ export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
x: e.movementX * mouseSensX * 0.0001,
y: e.movementY * mouseSensY * 0.0001
})
updateCursor()
bot.mouse.update()
updateMotion()
}

View file

@ -3,7 +3,6 @@
import fs from 'fs'
import { WorldRendererThree } from 'renderer/viewer/lib/worldrendererThree'
import { enable, disable, enabled } from 'debug'
import { getEntityCursor } from './worldInteractions'
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
@ -11,8 +10,8 @@ window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
return bot.world.getBlock(newPos)
}
window.cursorEntity = () => {
return getEntityCursor()
window.entityCursor = () => {
return bot.mouse.getCursorState().entity
}
// wanderer

2
src/globals.d.ts vendored
View file

@ -21,7 +21,7 @@ declare const loadedData: import('minecraft-data').IndexedData & { sounds: Recor
declare const customEvents: import('typed-emitter').default<{
/** Singleplayer load requested */
singleplayer (): void
digStart ()
digStart (): void
gameLoaded (): void
mineflayerBotCreated (): void
search (q: string): void

View file

@ -40,8 +40,6 @@ import { WorldDataEmitter, Viewer } from 'renderer/viewer'
import pathfinder from 'mineflayer-pathfinder'
import { Vec3 } from 'vec3'
import worldInteractions from './worldInteractions'
import * as THREE from 'three'
import MinecraftData from 'minecraft-data'
import debug from 'debug'
@ -106,13 +104,12 @@ import { parseFormattedMessagePacket } from './botUtils'
import { getViewerVersionData, getWsProtocolStream, handleCustomChannel } from './viewerConnector'
import { getWebsocketStream } from './mineflayer/websocket-core'
import { appQueryParams, appQueryParamsArray } from './appParams'
import { updateCursor } from './cameraRotationControls'
import { pingServerVersion } from './mineflayer/minecraft-protocol-extra'
import { playerState, PlayerStateManager } from './mineflayer/playerState'
import { states } from 'minecraft-protocol'
import { initMotionTracking } from './react/uiMotion'
import { UserError } from './mineflayer/userError'
import ping from './mineflayer/plugins/ping'
import mouse from './mineflayer/plugins/mouse'
import { LocalServer } from './customServer'
import { startLocalReplayServer } from './packetsReplay/replayPackets'
import { localRelayServerPlugin } from './mineflayer/plugins/packetsRecording'
@ -120,7 +117,6 @@ import { createFullScreenProgressReporter } from './core/progressReporter'
window.debug = debug
window.THREE = THREE
window.worldInteractions = worldInteractions
window.beforeRenderFrame = []
// ACTUAL CODE
@ -705,6 +701,7 @@ export async function connect (connectOptions: ConnectOptions) {
if (connectOptions.server) {
bot.loadPlugin(ping)
}
bot.loadPlugin(mouse)
if (!localReplaySession) {
bot.loadPlugin(localRelayServerPlugin)
}
@ -754,8 +751,6 @@ export async function connect (connectOptions: ConnectOptions) {
onBotCreate()
bot.once('login', () => {
worldInteractions.initBot()
setLoadingScreenStatus('Loading world')
const mcData = MinecraftData(bot.version)
@ -815,8 +810,6 @@ export async function connect (connectOptions: ConnectOptions) {
const worldView = window.worldView = new WorldDataEmitter(bot.world, renderDistance, center)
watchOptionsAfterWorldViewInit()
bot.on('physicsTick', () => updateCursor())
void initVR()
initMotionTracking()

View file

@ -0,0 +1,204 @@
import { createMouse } from 'mineflayer-mouse'
import * as THREE from 'three'
import { Bot } from 'mineflayer'
import { Block } from 'prismarine-block'
import { Vec3 } from 'vec3'
import { LineMaterial } from 'three-stdlib'
import { subscribeKey } from 'valtio/utils'
import { disposeObject } from 'renderer/viewer/lib/threeJsUtils'
import { isGameActive, showModal } from '../../globalState'
// wouldn't better to create atlas instead?
import destroyStage0 from '../../../assets/destroy_stage_0.png'
import destroyStage1 from '../../../assets/destroy_stage_1.png'
import destroyStage2 from '../../../assets/destroy_stage_2.png'
import destroyStage3 from '../../../assets/destroy_stage_3.png'
import destroyStage4 from '../../../assets/destroy_stage_4.png'
import destroyStage5 from '../../../assets/destroy_stage_5.png'
import destroyStage6 from '../../../assets/destroy_stage_6.png'
import destroyStage7 from '../../../assets/destroy_stage_7.png'
import destroyStage8 from '../../../assets/destroy_stage_8.png'
import destroyStage9 from '../../../assets/destroy_stage_9.png'
import { options } from '../../optionsStorage'
import { isCypress } from '../../standaloneUtils'
import { playerState } from '../playerState'
function createDisplayManager (bot: Bot, scene: THREE.Scene, renderer: THREE.WebGLRenderer) {
// State
const state = {
blockBreakMesh: null as THREE.Mesh | null,
breakTextures: [] as THREE.Texture[],
}
// Initialize break mesh and textures
const loader = new THREE.TextureLoader()
const destroyStagesImages = [
destroyStage0, destroyStage1, destroyStage2, destroyStage3, destroyStage4,
destroyStage5, destroyStage6, destroyStage7, destroyStage8, destroyStage9
]
for (let i = 0; i < 10; i++) {
const texture = loader.load(destroyStagesImages[i])
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
state.breakTextures.push(texture)
}
const breakMaterial = new THREE.MeshBasicMaterial({
transparent: true,
blending: THREE.MultiplyBlending,
alphaTest: 0.5,
})
state.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial)
state.blockBreakMesh.visible = false
state.blockBreakMesh.renderOrder = 999
state.blockBreakMesh.name = 'blockBreakMesh'
scene.add(state.blockBreakMesh)
// Update functions
function updateLineMaterial () {
const inCreative = bot.game.gameMode === 'creative'
const pixelRatio = viewer.renderer.getPixelRatio()
viewer.world.threejsCursorLineMaterial = new LineMaterial({
color: (() => {
switch (options.highlightBlockColor) {
case 'blue':
return 0x40_80_ff
case 'classic':
return 0x00_00_00
default:
return inCreative ? 0x40_80_ff : 0x00_00_00
}
})(),
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
// dashed: true,
// dashSize: 5,
})
}
function updateDisplay () {
if (viewer.world.threejsCursorLineMaterial) {
const { renderer } = viewer
viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750
}
}
beforeRenderFrame.push(updateDisplay)
// Update cursor line material on game mode change
bot.on('game', updateLineMaterial)
// Update material when highlight color setting changes
subscribeKey(options, 'highlightBlockColor', updateLineMaterial)
function updateBreakAnimation (block: Block | undefined, stage: number | null) {
hideBreakAnimation()
if (!state.blockBreakMesh) return // todo
if (stage === null || !block) return
const mergedShape = bot.mouse.getMergedCursorShape(block)
if (!mergedShape) return
const { position, width, height, depth } = bot.mouse.getDataFromShape(mergedShape)
state.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001)
position.add(block.position)
state.blockBreakMesh.position.set(position.x, position.y, position.z)
state.blockBreakMesh.visible = true
//@ts-expect-error
state.blockBreakMesh.material.map = state.breakTextures[stage] ?? state.breakTextures.at(-1)
//@ts-expect-error
state.blockBreakMesh.material.needsUpdate = true
}
function hideBreakAnimation () {
if (state.blockBreakMesh) {
state.blockBreakMesh.visible = false
}
}
function updateCursorBlock (data?: { block: Block }) {
if (!data?.block) {
viewer.world.setHighlightCursorBlock(null)
return
}
const { block } = data
viewer.world.setHighlightCursorBlock(block.position, bot.mouse.getBlockCursorShapes(block).map(shape => {
return bot.mouse.getDataFromShape(shape)
}))
}
bot.on('highlightCursorBlock', updateCursorBlock)
bot.on('blockBreakProgressStage', updateBreakAnimation)
bot.on('end', () => {
disposeObject(state.blockBreakMesh!, true)
scene.remove(state.blockBreakMesh!)
viewer.world.setHighlightCursorBlock(null)
})
}
export default (bot: Bot) => {
bot.loadPlugin(createMouse({}))
domListeners(bot)
createDisplayManager(bot, viewer.scene, viewer.renderer)
otherListeners()
}
const otherListeners = () => {
bot.on('startDigging', (block) => {
customEvents.emit('digStart')
})
bot.on('goingToSleep', () => {
showModal({ reactType: 'bed' })
})
bot.on('botArmSwingStart', (hand) => {
viewer.world.changeHandSwingingState(true, hand === 'left')
})
bot.on('botArmSwingEnd', (hand) => {
viewer.world.changeHandSwingingState(false, hand === 'left')
})
bot.on('startUsingItem', (item, slot, isOffhand, duration) => {
customEvents.emit('activateItem', item, isOffhand ? 45 : bot.quickBarSlot, isOffhand)
playerState.startUsingItem()
})
bot.on('stopUsingItem', () => {
playerState.stopUsingItem()
})
}
const domListeners = (bot: Bot) => {
document.addEventListener('mousedown', (e) => {
if (e.isTrusted && !document.pointerLockElement && !isCypress()) return
if (!isGameActive(true)) return
if (e.button === 0) {
bot.leftClickStart()
} else if (e.button === 2) {
bot.rightClickStart()
}
})
document.addEventListener('mouseup', (e) => {
if (e.button === 0) {
bot.leftClickEnd()
} else if (e.button === 2) {
bot.rightClickEnd()
}
})
bot.mouse.beforeUpdateChecks = () => {
if (!document.hasFocus()) {
// deactive all buttons
bot.mouse.buttons.fill(false)
}
}
}

View file

@ -60,6 +60,7 @@ const defaultOptions = {
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
customChannels: false,
packetsReplayAutoStart: false,
preciseMouseInput: false,
// todo ui setting, maybe enable by default?
waitForChunksRender: 'sp-only' as 'sp-only' | boolean,

View file

@ -3,7 +3,6 @@ import { subscribe, useSnapshot } from 'valtio'
import { useUtilsEffect } from '@zardoy/react-util'
import { options } from '../optionsStorage'
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
import worldInteractions from '../worldInteractions'
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
import { pointerLock, isInRealGameSession } from '../utils'
import { handleMovementStickDelta, joystickPointer } from './TouchAreasControls'
@ -152,7 +151,7 @@ function GameInteractionOverlayInner ({
virtualClickActive = false
} else if (!capturedPointer.active.activateCameraMove && (Date.now() - capturedPointer.active.time < touchStartBreakingBlockMs)) {
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
worldInteractions.update()
bot.mouse.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
}

View file

@ -1,7 +1,6 @@
import { CSSProperties, PointerEvent, useEffect, useRef } from 'react'
import { proxy, ref, useSnapshot } from 'valtio'
import { contro } from '../controls'
import worldInteractions from '../worldInteractions'
import { options } from '../optionsStorage'
import PixelartIcon from './PixelartIcon'
import Button from './Button'
@ -73,8 +72,9 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
}[name]
const holdDown = {
action () {
if (!bot) return
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
worldInteractions.update()
bot.mouse.update()
},
sneak () {
void contro.emit('trigger', {
@ -84,8 +84,9 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
active = bot?.getControlState('sneak')
},
break () {
if (!bot) return
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
worldInteractions.update()
bot.mouse.update()
active = true
},
jump () {
@ -108,8 +109,9 @@ export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props)
active = bot?.getControlState('sneak')
},
break () {
if (!bot) return
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
worldInteractions.update()
bot.mouse.update()
active = false
},
jump () {

View file

@ -1,551 +0,0 @@
//@ts-check
import * as THREE from 'three'
// wouldn't better to create atlas instead?
import { Vec3 } from 'vec3'
import { LineMaterial } from 'three-stdlib'
import { Entity } from 'prismarine-entity'
import { Block } from 'prismarine-block'
import { subscribeKey } from 'valtio/utils'
import destroyStage0 from '../assets/destroy_stage_0.png'
import destroyStage1 from '../assets/destroy_stage_1.png'
import destroyStage2 from '../assets/destroy_stage_2.png'
import destroyStage3 from '../assets/destroy_stage_3.png'
import destroyStage4 from '../assets/destroy_stage_4.png'
import destroyStage5 from '../assets/destroy_stage_5.png'
import destroyStage6 from '../assets/destroy_stage_6.png'
import destroyStage7 from '../assets/destroy_stage_7.png'
import destroyStage8 from '../assets/destroy_stage_8.png'
import destroyStage9 from '../assets/destroy_stage_9.png'
import { hideCurrentModal, isGameActive, showModal } from './globalState'
import { assertDefined } from './utils'
import { options } from './optionsStorage'
import { itemBeingUsed } from './react/Crosshair'
import { isCypress } from './standaloneUtils'
import { displayClientChat } from './botUtils'
import { playerState } from './mineflayer/playerState'
function getViewDirection (pitch, yaw) {
const csPitch = Math.cos(pitch)
const snPitch = Math.sin(pitch)
const csYaw = Math.cos(yaw)
const snYaw = Math.sin(yaw)
return new Vec3(-snYaw * csPitch, snPitch, -csYaw * csPitch)
}
class WorldInteraction {
ready = false
cursorBlock: Block | null = null
prevBreakState: number | null = null
currentDigTime: number | null = null
prevOnGround: boolean | null = null
lastBlockPlaced: number
lastSwing = 0
buttons = [false, false, false]
lastButtons = [false, false, false]
breakStartTime: number | undefined = 0
lastDugBlock: Vec3 | null = null
blockBreakMesh: THREE.Mesh
breakTextures: THREE.Texture[]
lastDigged: number
debugDigStatus: string
currentBreakBlock: { block: any, stage: number } | null = null
swingTimeout: any = null
oneTimeInit () {
const loader = new THREE.TextureLoader()
this.breakTextures = []
const destroyStagesImages = [
destroyStage0,
destroyStage1,
destroyStage2,
destroyStage3,
destroyStage4,
destroyStage5,
destroyStage6,
destroyStage7,
destroyStage8,
destroyStage9
]
for (let i = 0; i < 10; i++) {
const texture = loader.load(destroyStagesImages[i])
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
this.breakTextures.push(texture)
}
const breakMaterial = new THREE.MeshBasicMaterial({
transparent: true,
blending: THREE.MultiplyBlending,
alphaTest: 0.5,
})
this.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial)
this.blockBreakMesh.visible = false
this.blockBreakMesh.renderOrder = 999
this.blockBreakMesh.name = 'blockBreakMesh'
viewer.scene.add(this.blockBreakMesh)
// Setup events
document.addEventListener('mouseup', (e) => {
this.buttons[e.button] = false
})
this.lastBlockPlaced = 4 // ticks since last placed
document.addEventListener('mousedown', (e) => {
if (e.isTrusted && !document.pointerLockElement && !isCypress()) return
if (!isGameActive(true)) return
this.buttons[e.button] = true
const entity = getEntityCursor()
if (entity) {
if (e.button === 0) { // left click
bot.attack(entity)
} else if (e.button === 2) { // right click
this.activateEntity(entity)
}
}
})
document.addEventListener('blur', (e) => {
this.buttons = [false, false, false]
})
beforeRenderFrame.push(() => {
if (viewer.world.threejsCursorLineMaterial) {
const { renderer } = viewer
viewer.world.threejsCursorLineMaterial.resolution.set(renderer.domElement.width, renderer.domElement.height)
viewer.world.threejsCursorLineMaterial.dashOffset = performance.now() / 750
}
})
}
initBot () {
if (!this.ready) {
this.ready = true
this.oneTimeInit()
}
assertDefined(viewer)
bot.on('physicsTick', () => { if (this.lastBlockPlaced < 4) this.lastBlockPlaced++ })
bot.on('diggingCompleted', (block) => {
this.breakStartTime = undefined
this.lastDugBlock = block.position
// TODO: If the tool and enchantments immediately exceed the hardness times 30, the block breaks with no delay; SO WE NEED TO CHECK THAT
// TODO: Any blocks with a breaking time of 0.05
this.lastDigged = Date.now()
this.debugDigStatus = 'done'
this.stopBreakAnimation()
})
bot.on('diggingAborted', (block) => {
if (!viewer.world.cursorBlock?.equals(block.position)) return
this.debugDigStatus = 'aborted'
this.breakStartTime = undefined
if (this.buttons[0]) {
this.buttons[0] = false
this.update()
this.buttons[0] = true // trigger again
}
this.lastDugBlock = null
this.stopBreakAnimation()
})
bot.on('heldItemChanged' as any, () => {
itemBeingUsed.name = null
})
// Add new event listeners for block breaking and swinging
bot.on('entitySwingArm', (entity: Entity) => {
if (entity.id === bot.entity.id) {
if (this.swingTimeout) {
clearTimeout(this.swingTimeout)
}
bot.swingArm('right')
viewer.world.changeHandSwingingState(true, false)
this.swingTimeout = setTimeout(() => {
viewer.world.changeHandSwingingState(false, false)
this.swingTimeout = null
}, 250)
}
})
//@ts-expect-error mineflayer types are wrong
bot.on('blockBreakProgressObserved', (block: Block, destroyStage: number, entity: Entity) => {
if (this.cursorBlock?.position.equals(block.position) && entity.id === bot.entity.id) {
if (!this.buttons[0]) {
// Simulate left mouse button press
this.buttons[0] = true
this.update()
}
// this.setBreakState(block, destroyStage)
}
})
//@ts-expect-error mineflayer types are wrong
bot.on('blockBreakProgressEnd', (block: Block, entity: Entity) => {
if (this.currentBreakBlock?.block.position.equals(block.position) && entity.id === bot.entity.id) {
if (!this.buttons[0]) {
// Simulate left mouse button press
this.buttons[0] = false
this.update()
}
// this.stopBreakAnimation()
}
})
// Handle acknowledge_player_digging packet
bot._client.on('acknowledge_player_digging', (data: { location: { x: number, y: number, z: number }, block: number, status: number, successful: boolean } | { sequenceId: number }) => {
if ('location' in data && !data.successful) {
const packetPos = new Vec3(data.location.x, data.location.y, data.location.z)
if (this.cursorBlock?.position.equals(packetPos)) {
this.buttons[0] = false
this.update()
this.stopBreakAnimation()
}
}
})
const upLineMaterial = () => {
const inCreative = bot.game.gameMode === 'creative'
const pixelRatio = viewer.renderer.getPixelRatio()
viewer.world.threejsCursorLineMaterial = new LineMaterial({
color: (() => {
switch (options.highlightBlockColor) {
case 'blue':
return 0x40_80_ff
case 'classic':
return 0x00_00_00
default:
return inCreative ? 0x40_80_ff : 0x00_00_00
}
})(),
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
// dashed: true,
// dashSize: 5,
})
}
upLineMaterial()
// todo use gamemode update only
bot.on('game', upLineMaterial)
// Update material when highlight color setting changes
subscribeKey(options, 'highlightBlockColor', upLineMaterial)
}
activateEntity (entity: Entity) {
// mineflayer has completely wrong implementation of this action
if (bot.supportFeature('armAnimationBeforeUse')) {
bot.swingArm('right')
}
bot._client.write('use_entity', {
target: entity.id,
mouse: 2,
// todo do not fake
x: 0.581_012_585_759_162_9,
y: 0.581_012_585_759_162_9,
z: 0.581_012_585_759_162_9,
// x: raycastPosition.x - entity.position.x,
// y: raycastPosition.y - entity.position.y,
// z: raycastPosition.z - entity.position.z
sneaking: bot.getControlState('sneak'),
hand: 0
})
bot._client.write('use_entity', {
target: entity.id,
mouse: 0,
sneaking: bot.getControlState('sneak'),
hand: 0
})
if (!bot.supportFeature('armAnimationBeforeUse')) {
bot.swingArm('right')
}
}
beforeUpdateChecks () {
if (!document.hasFocus()) {
// deactive all buttson
this.buttons.fill(false)
}
}
// todo this shouldnt be done in the render loop, migrate the code to dom events to avoid delays on lags
update () {
this.beforeUpdateChecks()
const inSpectator = bot.game.gameMode === 'spectator'
const inAdventure = bot.game.gameMode === 'adventure'
const entity = getEntityCursor()
let _cursorBlock = inSpectator && !options.showCursorBlockInSpectator ? null : bot.blockAtCursor(5)
if (entity) {
_cursorBlock = null
}
this.cursorBlock = _cursorBlock
const { cursorBlock } = this
let cursorBlockDiggable = cursorBlock
if (cursorBlock && (!bot.canDigBlock(cursorBlock) || inAdventure) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
const cursorChanged = cursorBlock && viewer.world.cursorBlock ? !viewer.world.cursorBlock.equals(cursorBlock.position) : viewer.world.cursorBlock !== cursorBlock
// Place / interact / activate
if (this.buttons[2] && this.lastBlockPlaced >= 4) {
const activatableItems = (itemName: string) => {
return ['egg', 'fishing_rod', 'firework_rocket',
'fire_charge', 'snowball', 'ender_pearl', 'experience_bottle', 'potion',
'glass_bottle', 'bucket', 'water_bucket', 'lava_bucket', 'milk_bucket',
'minecart', 'boat', 'tnt_minecart', 'chest_minecart', 'hopper_minecart',
'command_block_minecart', 'armor_stand', 'lead', 'name_tag',
//
'writable_book', 'written_book', 'compass', 'clock', 'filled_map', 'empty_map', 'map',
'shears', 'carrot_on_a_stick', 'warped_fungus_on_a_stick',
'spawn_egg', 'trident', 'crossbow', 'elytra', 'shield', 'turtle_helmet', 'bow', 'crossbow', 'bucket_of_cod',
...loadedData.foodsArray.map((f) => f.name),
].includes(itemName)
}
const activate = bot.heldItem && activatableItems(bot.heldItem.name)
let stop = false
if (!bot.controlState.sneak) {
if (cursorBlock?.name === 'bed' || cursorBlock?.name.endsWith('_bed')) {
stop = true
showModal({ reactType: 'bed' })
let cancelSleep = true
void bot.sleep(cursorBlock).catch((e) => {
if (cancelSleep) {
hideCurrentModal()
}
// if (e.message === 'bot is not sleeping') return
displayClientChat(e.message)
})
setTimeout(() => {
cancelSleep = false
})
}
}
// todo placing with offhand
if (cursorBlock && !activate && !stop) {
const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)]
//@ts-expect-error
const delta = cursorBlock.intersect.minus(cursorBlock.position)
if (bot.heldItem) {
//@ts-expect-error todo
bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' }).catch(console.warn)
} else {
// https://discord.com/channels/413438066984747026/413438150594265099/1198724637572477098
const oldLookAt = bot.lookAt
//@ts-expect-error
bot.lookAt = (pos) => { }
//@ts-expect-error
// TODO it still must 1. fire block place 2. swing arm (right)
bot.activateBlock(cursorBlock, vecArray[cursorBlock.face], delta).finally(() => {
bot.lookAt = oldLookAt
}).catch(console.warn)
}
viewer.world.changeHandSwingingState(true, false)
viewer.world.changeHandSwingingState(false, false)
} else if (!stop) {
const offhand = activate ? false : activatableItems(bot.inventory.slots[45]?.name ?? '')
bot.activateItem(offhand) // todo offhand
const item = offhand ? bot.inventory.slots[45] : bot.heldItem
if (item) {
customEvents.emit('activateItem', item, offhand ? 45 : bot.quickBarSlot, offhand)
}
playerState.startUsingItem()
itemBeingUsed.name = (offhand ? bot.inventory.slots[45]?.name : bot.heldItem?.name) ?? null
itemBeingUsed.hand = offhand ? 1 : 0
}
this.lastBlockPlaced = 0
}
// stop using activated item (cancel)
if (itemBeingUsed.name && !this.buttons[2]) {
itemBeingUsed.name = null
// "only foods and bow can be deactivated" - not true, shields also can be deactivated and client always sends this
// if (bot.heldItem && (loadedData.foodsArray.map((f) => f.name).includes(bot.heldItem.name) || bot.heldItem.name === 'bow')) {
bot.deactivateItem()
playerState.stopUsingItem()
// }
}
// Stop break
if ((!this.buttons[0] && this.lastButtons[0]) || cursorChanged) {
try {
bot.stopDigging() // this shouldnt throw anything...
} catch (e) { } // to be reworked in mineflayer, then remove the try here
}
// We stopped breaking
if ((!this.buttons[0] && this.lastButtons[0])) {
this.lastDugBlock = null
this.breakStartTime = undefined
this.debugDigStatus = 'cancelled'
this.stopBreakAnimation()
}
const onGround = bot.entity.onGround || bot.game.gameMode === 'creative'
this.prevOnGround ??= onGround // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down // todo this should be fixed in mineflayer to involve correct calculations when this changes as this is very important when mining straight down
// Start break
// todo last check doesnt work as cursorChanged happens once (after that check is false)
if (
this.buttons[0]
) {
if (cursorBlockDiggable
&& (!this.lastButtons[0] || ((cursorChanged || (this.lastDugBlock && !this.lastDugBlock.equals(cursorBlock!.position))) && Date.now() - (this.lastDigged ?? 0) > 300) || onGround !== this.prevOnGround)
&& onGround) {
this.lastDugBlock = null
this.debugDigStatus = 'breaking'
this.currentDigTime = bot.digTime(cursorBlockDiggable)
this.breakStartTime = performance.now()
const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)]
bot.dig(
//@ts-expect-error
cursorBlockDiggable, 'ignore', vecArray[cursorBlockDiggable.face]
).catch((err) => {
if (err.message === 'Digging aborted') return
throw err
})
customEvents.emit('digStart')
this.lastDigged = Date.now()
viewer.world.changeHandSwingingState(true, false)
} else if (performance.now() - this.lastSwing > 200) {
bot.swingArm('right')
this.lastSwing = performance.now()
}
}
if (!this.buttons[0] && this.lastButtons[0]) {
viewer.world.changeHandSwingingState(false, false)
}
this.prevOnGround = onGround
// Show cursor
const allShapes = [...cursorBlock?.shapes ?? [], ...cursorBlock?.['interactionShapes'] ?? []]
if (cursorBlock) {
// BREAK MESH
// union of all values
const breakShape = allShapes.reduce((acc, cur) => {
return [
Math.min(acc[0], cur[0]),
Math.min(acc[1], cur[1]),
Math.min(acc[2], cur[2]),
Math.max(acc[3], cur[3]),
Math.max(acc[4], cur[4]),
Math.max(acc[5], cur[5])
]
})
const { position, width, height, depth } = getDataFromShape(breakShape)
this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001)
position.add(cursorBlock.position)
this.blockBreakMesh.position.set(position.x, position.y, position.z)
}
// Show break animation
if (cursorBlockDiggable && this.breakStartTime && bot.game.gameMode !== 'creative') {
const elapsed = performance.now() - this.breakStartTime
const time = bot.digTime(cursorBlockDiggable)
if (time !== this.currentDigTime) {
console.warn('dig time changed! cancelling!', time, 'from', this.currentDigTime) // todo
try { bot.stopDigging() } catch { }
}
const state = Math.floor((elapsed / time) * 10)
if (state !== this.prevBreakState) {
this.setBreakState(cursorBlockDiggable, Math.min(state, 9))
}
this.prevBreakState = state
} else {
this.blockBreakMesh.visible = false
}
// Update state
if (cursorChanged) {
viewer.world.setHighlightCursorBlock(cursorBlock?.position ?? null, allShapes.map(shape => {
return getDataFromShape(shape)
}))
}
this.lastButtons[0] = this.buttons[0]
this.lastButtons[1] = this.buttons[1]
this.lastButtons[2] = this.buttons[2]
}
setBreakState (block: Block, stage: number) {
this.currentBreakBlock = { block, stage }
this.blockBreakMesh.visible = true
//@ts-expect-error
this.blockBreakMesh.material.map = this.breakTextures[stage] ?? this.breakTextures.at(-1)
//@ts-expect-error
this.blockBreakMesh.material.needsUpdate = true
}
stopBreakAnimation () {
this.currentBreakBlock = null
this.blockBreakMesh.visible = false
}
}
const getDataFromShape = (shape) => {
const width = shape[3] - shape[0]
const height = shape[4] - shape[1]
const depth = shape[5] - shape[2]
const centerX = (shape[3] + shape[0]) / 2
const centerY = (shape[4] + shape[1]) / 2
const centerZ = (shape[5] + shape[2]) / 2
const position = new Vec3(centerX, centerY, centerZ)
return { position, width, height, depth }
}
// Blocks that can be interacted with in adventure mode
const activatableBlockPatterns = [
// Containers
/^(chest|barrel|hopper|dispenser|dropper)$/,
/^.*shulker_box$/,
/^.*(furnace|smoker)$/,
/^(brewing_stand|beacon)$/,
// Crafting
/^.*table$/,
/^(grindstone|stonecutter|loom)$/,
/^.*anvil$/,
// Redstone
/^(lever|repeater|comparator|daylight_detector|observer|note_block|jukebox|bell)$/,
// Buttons
/^.*button$/,
// Doors and trapdoors
/^.*door$/,
/^.*trapdoor$/,
// Functional blocks
/^(enchanting_table|lectern|composter|respawn_anchor|lodestone|conduit)$/,
/^.*bee.*$/,
// Beds
/^.*bed$/,
// Misc
/^(cake|decorated_pot|crafter|trial_spawner|vault)$/
]
function isBlockActivatable (blockName: string) {
return activatableBlockPatterns.some(pattern => pattern.test(blockName))
}
function isLookingAtActivatableBlock (block: Block) {
return isBlockActivatable(block.name)
}
export const getEntityCursor = () => {
const entity = bot.nearestEntity((e) => {
if (e.position.distanceTo(bot.entity.position) <= (bot.game.gameMode === 'creative' ? 5 : 3)) {
const dir = getViewDirection(bot.entity.pitch, bot.entity.yaw)
const { width, height } = e
const { x: eX, y: eY, z: eZ } = e.position
const { x: bX, y: bY, z: bZ } = bot.entity.position
const box = new THREE.Box3(
new THREE.Vector3(eX - width / 2, eY, eZ - width / 2),
new THREE.Vector3(eX + width / 2, eY + height, eZ + width / 2)
)
const r = new THREE.Raycaster(
new THREE.Vector3(bX, bY + 1.52, bZ),
new THREE.Vector3(dir.x, dir.y, dir.z)
)
const int = r.ray.intersectBox(box, new THREE.Vector3(eX, eY, eZ))
return int !== null
}
return false
})
return entity
}
const worldInteraction = new WorldInteraction()
globalThis.worldInteraction = worldInteraction
export default worldInteraction