feat: Add videos/images from source with protocol extension (#301)

This commit is contained in:
Vitaly 2025-03-24 20:17:54 +03:00 committed by GitHub
commit 563f5fa007
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 712 additions and 8 deletions

View file

@ -16,6 +16,18 @@ import { IPlayerState } from './basePlayerState'
import { getMesh } from './entity/EntityMesh'
import { armorModel } from './entity/armorModels'
interface MediaProperties {
position: { x: number, y: number, z: number }
size: { width: number, height: number }
src: string
rotation?: 0 | 1 | 2 | 3 // 0-3 for 0°, 90°, 180°, 270°
doubleSide?: boolean
background?: number // Hexadecimal color (e.g., 0x000000 for black)
opacity?: number // 0-1 value for transparency
uvMapping?: { startU: number, endU: number, startV: number, endV: number }
allowOrigins?: string[] | boolean
}
export class WorldRendererThree extends WorldRendererCommon {
interactionLines: null | { blockPos; mesh } = null
outputFormat = 'threeJs' as const
@ -28,6 +40,12 @@ export class WorldRendererThree extends WorldRendererCommon {
holdingBlock: HoldingBlock
holdingBlockLeft: HoldingBlock
rendererDevice = '...'
customMedia = new Map<string, {
mesh: THREE.Object3D
video: HTMLVideoElement | undefined
texture: THREE.Texture
updateUVMapping: (config: { startU: number, endU: number, startV: number, endV: number }) => void
}>()
get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
@ -457,6 +475,399 @@ export class WorldRendererThree extends WorldRendererCommon {
console.warn('Failed to get renderer info', err)
}
}
private createErrorTexture (width: number, height: number, background = 0x00_00_00): THREE.CanvasTexture {
const canvas = document.createElement('canvas')
// Scale up the canvas size for better text quality
canvas.width = width * 100
canvas.height = height * 100
const ctx = canvas.getContext('2d')
if (!ctx) return new THREE.CanvasTexture(canvas)
// Clear with transparent background
ctx.clearRect(0, 0, canvas.width, canvas.height)
// Add background color
ctx.fillStyle = `rgba(${background >> 16 & 255}, ${background >> 8 & 255}, ${background & 255}, 0.5)`
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Add red text
ctx.fillStyle = '#ff0000'
ctx.font = 'bold 10px sans-serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Failed to load', canvas.width / 2, canvas.height / 2, canvas.width)
const texture = new THREE.CanvasTexture(canvas)
texture.minFilter = THREE.LinearFilter
texture.magFilter = THREE.LinearFilter
return texture
}
private createBackgroundTexture (width: number, height: number, color = 0x00_00_00, opacity = 1): THREE.CanvasTexture {
const canvas = document.createElement('canvas')
canvas.width = 1
canvas.height = 1
const ctx = canvas.getContext('2d')
if (!ctx) return new THREE.CanvasTexture(canvas)
// Convert hex color to rgba
const r = (color >> 16) & 255
const g = (color >> 8) & 255
const b = color & 255
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`
ctx.fillRect(0, 0, 1, 1)
const texture = new THREE.CanvasTexture(canvas)
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
return texture
}
validateOrigin (src: string, allowOrigins: string[] | boolean) {
if (allowOrigins === true) return true
if (allowOrigins === false) return false
const url = new URL(src)
return allowOrigins.some(origin => url.origin.endsWith(origin))
}
addMedia (id: string, props: MediaProperties) {
this.destroyMedia(id)
const originSecurityError = props.allowOrigins !== undefined && !this.validateOrigin(props.src, props.allowOrigins)
if (originSecurityError) {
console.warn('Remote resource blocked due to security policy', props.src, 'allowed origins:', props.allowOrigins, 'you can control it with `remoteContentNotSameOrigin` option')
props.src = ''
}
const isImage = props.src.endsWith('.png') || props.src.endsWith('.jpg') || props.src.endsWith('.jpeg')
let video: HTMLVideoElement | undefined
if (!isImage) {
video = document.createElement('video')
video.src = props.src
video.loop = true
video.muted = true
video.playsInline = true
video.crossOrigin = 'anonymous'
}
// Create background texture first
const backgroundTexture = this.createBackgroundTexture(
props.size.width,
props.size.height,
props.background,
// props.opacity ?? 1
)
const handleError = () => {
const errorTexture = this.createErrorTexture(props.size.width, props.size.height, props.background)
material.map = errorTexture
material.needsUpdate = true
}
// Create a plane geometry with configurable UV mapping
const geometry = new THREE.PlaneGeometry(1, 1)
// Create material with initial properties using background texture
const material = new THREE.MeshBasicMaterial({
map: backgroundTexture,
transparent: true,
side: props.doubleSide ? THREE.DoubleSide : THREE.FrontSide
})
const texture = video
? new THREE.VideoTexture(video)
: new THREE.TextureLoader().load(props.src, () => {
if (this.customMedia.get(id)?.texture === texture) {
material.map = texture
material.needsUpdate = true
}
}, undefined, handleError) // todo cache
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
texture.format = THREE.RGBAFormat
texture.generateMipmaps = false
// Create inner mesh for offsets
const mesh = new THREE.Mesh(geometry, material)
const { mesh: panel } = this.positionMeshExact(mesh, THREE.MathUtils.degToRad((props.rotation ?? 0) * 90), props.position, props.size.width, props.size.height)
this.scene.add(panel)
if (video) {
// Start playing the video
video.play().catch(err => {
console.error('Failed to play video:', err)
handleError()
})
// Update texture in animation loop
mesh.onBeforeRender = () => {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
if (material.map !== texture) {
material.map = texture
material.needsUpdate = true
}
texture.needsUpdate = true
}
}
}
// UV mapping configuration
const updateUVMapping = (config: { startU: number, endU: number, startV: number, endV: number }) => {
const uvs = geometry.attributes.uv.array as Float32Array
uvs[0] = config.startU
uvs[1] = config.startV
uvs[2] = config.endU
uvs[3] = config.startV
uvs[4] = config.endU
uvs[5] = config.endV
uvs[6] = config.startU
uvs[7] = config.endV
geometry.attributes.uv.needsUpdate = true
}
// Apply initial UV mapping if provided
if (props.uvMapping) {
updateUVMapping(props.uvMapping)
}
// Store video data
this.customMedia.set(id, {
mesh: panel,
video,
texture,
updateUVMapping
})
return id
}
setVideoPlaying (id: string, playing: boolean) {
const videoData = this.customMedia.get(id)
if (videoData?.video) {
if (playing) {
videoData.video.play().catch(console.error)
} else {
videoData.video.pause()
}
}
}
setVideoSeeking (id: string, seconds: number) {
const videoData = this.customMedia.get(id)
if (videoData?.video) {
videoData.video.currentTime = seconds
}
}
setVideoVolume (id: string, volume: number) {
const videoData = this.customMedia.get(id)
if (videoData?.video) {
videoData.video.volume = volume
}
}
setVideoSpeed (id: string, speed: number) {
const videoData = this.customMedia.get(id)
if (videoData?.video) {
videoData.video.playbackRate = speed
}
}
destroyMedia (id: string) {
const media = this.customMedia.get(id)
if (media) {
if (media.video) {
media.video.pause()
media.video.src = ''
}
this.scene.remove(media.mesh)
media.texture.dispose()
// Get the inner mesh from the group
const mesh = media.mesh.children[0] as THREE.Mesh
if (mesh) {
mesh.geometry.dispose()
if (mesh.material instanceof THREE.Material) {
mesh.material.dispose()
}
}
this.customMedia.delete(id)
}
}
/**
* Positions a mesh exactly at startPosition and extends it along the rotation direction
* with the specified width and height
*
* @param mesh The mesh to position
* @param rotation Rotation in radians (applied to Y axis)
* @param startPosition The exact starting position (corner) of the mesh
* @param width Width of the mesh
* @param height Height of the mesh
* @param depth Depth of the mesh (default: 1)
* @returns The positioned mesh for chaining
*/
positionMeshExact (
mesh: THREE.Mesh,
rotation: number,
startPosition: { x: number, y: number, z: number },
width: number,
height: number,
depth = 1
) {
// avoid z-fighting with the ground plane
if (rotation === 0) {
startPosition.z += 0.001
}
if (rotation === Math.PI / 2) {
startPosition.x -= 0.001
}
if (rotation === Math.PI) {
startPosition.z -= 0.001
}
if (rotation === 3 * Math.PI / 2) {
startPosition.x += 0.001
}
// rotation normalize coordinates
if (rotation === 0) {
startPosition.z += 1
}
if (rotation === Math.PI) {
startPosition.x += 1
}
if (rotation === 3 * Math.PI / 2) {
startPosition.z += 1
startPosition.x += 1
}
// First, clean up any previous transformations
mesh.matrix.identity()
mesh.position.set(0, 0, 0)
mesh.rotation.set(0, 0, 0)
mesh.scale.set(1, 1, 1)
// By default, PlaneGeometry creates a plane in the XY plane (facing +Z)
// We need to set up the proper orientation for our use case
// Rotate the plane to face the correct direction based on the rotation parameter
mesh.rotateY(rotation)
if (rotation === Math.PI / 2 || rotation === 3 * Math.PI / 2) {
mesh.rotateZ(-Math.PI)
mesh.rotateX(-Math.PI)
}
// Scale it to the desired size
mesh.scale.set(width, height, depth)
// For a PlaneGeometry, if we want the corner at the origin, we need to offset
// by half the dimensions after scaling
mesh.geometry.translate(0.5, 0.5, 0)
mesh.geometry.attributes.position.needsUpdate = true
// Now place the mesh at the start position
mesh.position.set(startPosition.x, startPosition.y, startPosition.z)
// Create a group to hold our mesh and markers
const debugGroup = new THREE.Group()
debugGroup.add(mesh)
// Add a marker at the starting position (should be exactly at pos)
const startMarker = new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.1, 0.1),
new THREE.MeshBasicMaterial({ color: 0xff_00_00 })
)
startMarker.position.copy(new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z))
debugGroup.add(startMarker)
// Add a marker at the end position (width units away in the rotated direction)
const endX = startPosition.x + Math.cos(rotation) * width
const endZ = startPosition.z + Math.sin(rotation) * width
const endYMarker = new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.1, 0.1),
new THREE.MeshBasicMaterial({ color: 0x00_00_ff })
)
endYMarker.position.set(startPosition.x, startPosition.y + height, startPosition.z)
debugGroup.add(endYMarker)
// Add a marker at the width endpoint
const endWidthMarker = new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.1, 0.1),
new THREE.MeshBasicMaterial({ color: 0xff_ff_00 })
)
endWidthMarker.position.set(endX, startPosition.y, endZ)
debugGroup.add(endWidthMarker)
// Add a marker at the corner diagonal endpoint (both width and height)
const endCornerMarker = new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.1, 0.1),
new THREE.MeshBasicMaterial({ color: 0xff_00_ff })
)
endCornerMarker.position.set(endX, startPosition.y + height, endZ)
debugGroup.add(endCornerMarker)
// Also add a visual helper to show the rotation direction
const directionHelper = new THREE.ArrowHelper(
new THREE.Vector3(Math.cos(rotation), 0, Math.sin(rotation)),
new THREE.Vector3(startPosition.x, startPosition.y, startPosition.z),
1,
0xff_00_00
)
debugGroup.add(directionHelper)
return {
mesh,
debugGroup
}
}
createTestCanvasTexture () {
const canvas = document.createElement('canvas')
canvas.width = 100
canvas.height = 100
const ctx = canvas.getContext('2d')
if (!ctx) return null
ctx.font = '10px Arial'
ctx.fillStyle = 'red'
ctx.fillText('Hello World', 0, 10) // at
return new THREE.CanvasTexture(canvas)
}
/**
* Creates a test mesh that demonstrates the exact positioning
*/
addTestMeshExact (rotationNum: number) {
const pos = window.cursorBlockRel().position
console.log('Creating exact positioned test mesh at:', pos)
// Create a plane mesh with a wireframe to visualize boundaries
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(1, 1),
new THREE.MeshBasicMaterial({
// side: THREE.DoubleSide,
map: this.createTestCanvasTexture()
})
)
const width = 2
const height = 1
const rotation = THREE.MathUtils.degToRad(rotationNum * 90) // 90 degrees in radians
// Position the mesh exactly where we want it
const { debugGroup } = this.positionMeshExact(plane, rotation, pos, width, height)
viewer.scene.add(debugGroup)
console.log('Exact test mesh added with dimensions:', width, height, 'and rotation:', rotation)
}
}
class StarField {

View file

@ -1,5 +1,9 @@
import { Vec3 } from 'vec3'
import PItem from 'prismarine-item'
import * as THREE from 'three'
import { WorldRendererThree } from '../renderer/viewer/lib/worldrendererThree'
import { options } from './optionsStorage'
import { jeiCustomCategories } from './inventoryWindows'
customEvents.on('mineflayerBotCreated', async () => {
if (!options.customChannels) return
@ -9,6 +13,8 @@ customEvents.on('mineflayerBotCreated', async () => {
})
})
registerBlockModelsChannel()
registerMediaChannels()
registeredJeiChannel()
})
const registerBlockModelsChannel = () => {
@ -88,7 +94,7 @@ const registeredJeiChannel = () => {
type: ['pstring', { countType: 'i16' }]
},
{
name: 'categoryTitle',
name: '_categoryTitle',
type: ['pstring', { countType: 'i16' }]
},
{
@ -102,8 +108,280 @@ const registeredJeiChannel = () => {
bot._client.on(CHANNEL_NAME as any, (data) => {
const { id, categoryTitle, items } = data
// ...
if (items === '') {
// remove category
jeiCustomCategories.value = jeiCustomCategories.value.filter(x => x.id !== id)
return
}
const PrismarineItem = PItem(bot.version)
jeiCustomCategories.value.push({
id,
categoryTitle,
items: JSON.parse(items).map(x => {
const itemString = x.itemName || x.item_name || x.item || x.itemId
const itemId = loadedData.itemsByName[itemString.replace('minecraft:', '')]
if (!itemId) {
console.warn(`Could not add item ${itemString} to JEI category ${categoryTitle} because it was not found`)
return null
}
// const item = new PrismarineItem(itemId.id, x.itemCount || x.item_count || x.count || 1, x.itemDamage || x.item_damage || x.damage || 0, x.itemNbt || x.item_nbt || x.nbt || null)
return PrismarineItem.fromNotch({
...x,
itemId: itemId.id,
})
})
})
})
console.debug(`registered custom channel ${CHANNEL_NAME} channel`)
}
const registerMediaChannels = () => {
// Media Add Channel
const ADD_CHANNEL = 'minecraft-web-client:media-add'
const addPacketStructure = [
'container',
[
{ name: 'id', type: ['pstring', { countType: 'i16' }] },
{ name: 'x', type: 'f32' },
{ name: 'y', type: 'f32' },
{ name: 'z', type: 'f32' },
{ name: 'width', type: 'f32' },
{ name: 'height', type: 'f32' },
// N, 0
// W, 3
// S, 2
// E, 1
{ name: 'rotation', type: 'i16' }, // 0: 0° - towards positive z, 1: 90° - positive x, 2: 180° - negative z, 3: 270° - negative x (3-6 is same but double side)
{ name: 'source', type: ['pstring', { countType: 'i16' }] },
{ name: 'loop', type: 'bool' },
{ name: '_volume', type: 'f32' }, // 0
{ name: '_aspectRatioMode', type: 'i16' }, // 0
{ name: '_background', type: 'i16' }, // 0
{ name: '_opacity', type: 'i16' }, // 1
{ name: '_cropXStart', type: 'f32' }, // 0
{ name: '_cropYStart', type: 'f32' }, // 0
{ name: '_cropXEnd', type: 'f32' }, // 0
{ name: '_cropYEnd', type: 'f32' }, // 0
]
]
// Media Control Channels
const PLAY_CHANNEL = 'minecraft-web-client:media-play'
const PAUSE_CHANNEL = 'minecraft-web-client:media-pause'
const SEEK_CHANNEL = 'minecraft-web-client:media-seek'
const VOLUME_CHANNEL = 'minecraft-web-client:media-volume'
const SPEED_CHANNEL = 'minecraft-web-client:media-speed'
const DESTROY_CHANNEL = 'minecraft-web-client:media-destroy'
const noDataPacketStructure = [
'container',
[
{ name: 'id', type: ['pstring', { countType: 'i16' }] }
]
]
const setNumberPacketStructure = [
'container',
[
{ name: 'id', type: ['pstring', { countType: 'i16' }] },
{ name: 'seconds', type: 'f32' }
]
]
// Register channels
bot._client.registerChannel(ADD_CHANNEL, addPacketStructure, true)
bot._client.registerChannel(PLAY_CHANNEL, noDataPacketStructure, true)
bot._client.registerChannel(PAUSE_CHANNEL, noDataPacketStructure, true)
bot._client.registerChannel(SEEK_CHANNEL, setNumberPacketStructure, true)
bot._client.registerChannel(VOLUME_CHANNEL, setNumberPacketStructure, true)
bot._client.registerChannel(SPEED_CHANNEL, setNumberPacketStructure, true)
bot._client.registerChannel(DESTROY_CHANNEL, noDataPacketStructure, true)
// Handle media add
bot._client.on(ADD_CHANNEL as any, (data) => {
const { id, x, y, z, width, height, rotation, source, loop, background, opacity } = data
const worldRenderer = viewer.world as WorldRendererThree
// Destroy existing video if it exists
worldRenderer.destroyMedia(id)
// Add new video
worldRenderer.addMedia(id, {
position: { x, y, z },
size: { width, height },
// side: 'towards',
src: source,
rotation: rotation as 0 | 1 | 2 | 3,
doubleSide: false,
background,
opacity: opacity / 100,
allowOrigins: options.remoteContentNotSameOrigin === false ? [getCurrentTopDomain()] : options.remoteContentNotSameOrigin
})
// Set loop state
if (!loop) {
const videoData = worldRenderer.customMedia.get(id)
if (videoData?.video) {
videoData.video.loop = false
}
}
})
// Handle media play
bot._client.on(PLAY_CHANNEL as any, (data) => {
const { id } = data
const worldRenderer = viewer.world as WorldRendererThree
worldRenderer.setVideoPlaying(id, true)
})
// Handle media pause
bot._client.on(PAUSE_CHANNEL as any, (data) => {
const { id } = data
const worldRenderer = viewer.world as WorldRendererThree
worldRenderer.setVideoPlaying(id, false)
})
// Handle media seek
bot._client.on(SEEK_CHANNEL as any, (data) => {
const { id, seconds } = data
const worldRenderer = viewer.world as WorldRendererThree
worldRenderer.setVideoSeeking(id, seconds)
})
// Handle media destroy
bot._client.on(DESTROY_CHANNEL as any, (data) => {
const { id } = data
const worldRenderer = viewer.world as WorldRendererThree
worldRenderer.destroyMedia(id)
})
// Handle media volume
bot._client.on(VOLUME_CHANNEL as any, (data) => {
const { id, volume } = data
const worldRenderer = viewer.world as WorldRendererThree
worldRenderer.setVideoVolume(id, volume)
})
// Handle media speed
bot._client.on(SPEED_CHANNEL as any, (data) => {
const { id, speed } = data
const worldRenderer = viewer.world as WorldRendererThree
worldRenderer.setVideoSpeed(id, speed)
})
// ---
// Video interaction channel
const interactionPacketStructure = [
'container',
[
{ name: 'id', type: ['pstring', { countType: 'i16' }] },
{ name: 'x', type: 'f32' },
{ name: 'y', type: 'f32' },
{ name: 'isRightClick', type: 'bool' }
]
]
bot._client.registerChannel(MEDIA_INTERACTION_CHANNEL, interactionPacketStructure, true)
console.debug('Registered media channels')
}
const MEDIA_INTERACTION_CHANNEL = 'minecraft-web-client:media-interaction'
export const sendVideoInteraction = (id: string, x: number, y: number, isRightClick: boolean) => {
bot._client.writeChannel(MEDIA_INTERACTION_CHANNEL, { id, x, y, isRightClick })
}
export const videoCursorInteraction = () => {
const worldRenderer = viewer.world as WorldRendererThree
const { camera } = worldRenderer
const raycaster = new THREE.Raycaster()
// Get mouse position at center of screen
const mouse = new THREE.Vector2(0, 0)
// Update the raycaster
raycaster.setFromCamera(mouse, camera)
// Check intersection with all video meshes
for (const [id, videoData] of worldRenderer.customMedia.entries()) {
// Get the actual mesh (first child of the group)
const mesh = videoData.mesh.children[0] as THREE.Mesh
if (!mesh) continue
const intersects = raycaster.intersectObject(mesh, false)
if (intersects.length > 0) {
const intersection = intersects[0]
const { uv } = intersection
if (!uv) return null
return {
id,
x: uv.x,
y: uv.y
}
}
}
return null
}
window.videoCursorInteraction = videoCursorInteraction
const addTestVideo = (rotation = 0 as 0 | 1 | 2 | 3, scale = 1, isImage = false) => {
const block = window.cursorBlockRel()
if (!block) return
const { position: startPosition } = block
const worldRenderer = viewer.world as WorldRendererThree
// Add video with proper positioning
worldRenderer.addMedia('test-video', {
position: {
x: startPosition.x,
y: startPosition.y + 1,
z: startPosition.z
},
size: {
width: scale,
height: scale
},
src: isImage ? 'https://bucket.mcraft.fun/test_image.png' : 'https://bucket.mcraft.fun/test_video.mp4',
rotation,
// doubleSide: true,
background: 0x00_00_00, // Black color
// TODO broken
// uvMapping: {
// startU: 0,
// endU: 1,
// startV: 0,
// endV: 1
// },
opacity: 1,
allowOrigins: true,
})
}
window.addTestVideo = addTestVideo
function getCurrentTopDomain (): string {
const { hostname } = location
// Split hostname into parts
const parts = hostname.split('.')
// Handle special cases like co.uk, com.br, etc.
if (parts.length > 2) {
// Check for common country codes with additional segments
if (parts.at(-2) === 'co' ||
parts.at(-2) === 'com' ||
parts.at(-2) === 'org' ||
parts.at(-2) === 'gov') {
// Return last 3 parts (e.g., example.co.uk)
return parts.slice(-3).join('.')
}
}
// Return last 2 parts (e.g., example.com)
return parts.slice(-2).join('.')
}

View file

@ -22,6 +22,7 @@ import destroyStage9 from '../../../assets/destroy_stage_9.png'
import { options } from '../../optionsStorage'
import { isCypress } from '../../standaloneUtils'
import { playerState } from '../playerState'
import { sendVideoInteraction, videoCursorInteraction } from '../../customChannels'
function createDisplayManager (bot: Bot, scene: THREE.Scene, renderer: THREE.WebGLRenderer) {
// State
@ -180,6 +181,12 @@ const domListeners = (bot: Bot) => {
if (e.isTrusted && !document.pointerLockElement && !isCypress()) return
if (!isGameActive(true)) return
const videoInteraction = videoCursorInteraction()
if (videoInteraction) {
sendVideoInteraction(videoInteraction.id, videoInteraction.x, videoInteraction.y, e.button === 0)
return
}
if (e.button === 0) {
bot.leftClickStart()
} else if (e.button === 2) {

View file

@ -57,6 +57,7 @@ const defaultOptions = {
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
customChannels: false,
remoteContentNotSameOrigin: false as boolean | string[],
packetsReplayAutoStart: false,
preciseMouseInput: false,
// todo ui setting, maybe enable by default?

View file

@ -5,6 +5,7 @@ import { options } from '../optionsStorage'
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
import { pointerLock, isInRealGameSession } from '../utils'
import { videoCursorInteraction } from '../customChannels'
import { handleMovementStickDelta, joystickPointer } from './TouchAreasControls'
/** after what time of holding the finger start breaking the block */
@ -153,7 +154,7 @@ function GameInteractionOverlayInner ({
// single click action
const MOUSE_BUTTON_RIGHT = 2
const MOUSE_BUTTON_LEFT = 0
const gonnaAttack = !!bot.mouse.getCursorState().entity
const gonnaAttack = !!bot.mouse.getCursorState().entity || !!videoCursorInteraction()
document.dispatchEvent(new MouseEvent('mousedown', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
bot.mouse.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))

View file

@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useSnapshot } from 'valtio'
import { options } from '../optionsStorage'
import { activeModalStack } from '../globalState'
import { videoCursorInteraction } from '../customChannels'
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import styles from './TouchInteractionHint.module.css'
import { useUsingTouch } from './utilsApp'
@ -14,12 +15,17 @@ export default () => {
useEffect(() => {
const update = () => {
const cursorState = bot.mouse.getCursorState()
if (cursorState.entity) {
const entityName = cursorState.entity.displayName ?? cursorState.entity.name
setHintText(`Attack ${entityName}`)
const videoInteraction = videoCursorInteraction()
if (videoInteraction) {
setHintText(`Interact with video`)
} else {
setHintText(null)
const cursorState = bot.mouse.getCursorState()
if (cursorState.entity) {
const entityName = cursorState.entity.displayName ?? cursorState.entity.name
setHintText(`Attack ${entityName}`)
} else {
setHintText(null)
}
}
}