feat: Refactor mouse controls, fixing all false entity/item interaction issues (#286)
This commit is contained in:
parent
fa9c0813c3
commit
ceb4cb0b66
11 changed files with 1066 additions and 594 deletions
|
|
@ -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
858
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
2
src/globals.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
11
src/index.ts
11
src/index.ts
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
204
src/mineflayer/plugins/mouse.ts
Normal file
204
src/mineflayer/plugins/mouse.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue