diff --git a/experiments/three.html b/experiments/three.html
new file mode 100644
index 000000000..8765081b5
--- /dev/null
+++ b/experiments/three.html
@@ -0,0 +1 @@
+
diff --git a/experiments/three.ts b/experiments/three.ts
new file mode 100644
index 000000000..7a629a132
--- /dev/null
+++ b/experiments/three.ts
@@ -0,0 +1,101 @@
+import * as THREE from 'three'
+import * as tweenJs from '@tweenjs/tween.js'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+import * as THREE from 'three';
+import Jimp from 'jimp';
+
+const scene = new THREE.Scene()
+const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
+camera.position.set(0, 0, 5)
+const renderer = new THREE.WebGLRenderer()
+renderer.setSize(window.innerWidth, window.innerHeight)
+document.body.appendChild(renderer.domElement)
+
+const controls = new OrbitControls(camera, renderer.domElement)
+
+const geometry = new THREE.BoxGeometry(1, 1, 1)
+const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
+const cube = new THREE.Mesh(geometry, material)
+cube.position.set(0.5, 0.5, 0.5);
+const group = new THREE.Group()
+group.add(cube)
+group.position.set(-0.5, -0.5, -0.5);
+const outerGroup = new THREE.Group()
+outerGroup.add(group)
+outerGroup.scale.set(0.2, 0.2, 0.2)
+outerGroup.position.set(1, 1, 0)
+scene.add(outerGroup)
+
+// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
+// mesh.position.set(0.5, 1, 0.5)
+// const group = new THREE.Group()
+// group.add(mesh)
+// group.position.set(-0.5, -1, -0.5)
+// const outerGroup = new THREE.Group()
+// outerGroup.add(group)
+// // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
+// scene.add(outerGroup)
+
+ new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
+
+const tweenGroup = new tweenJs.Group()
+function animate () {
+ tweenGroup.update()
+ requestAnimationFrame(animate)
+// cube.rotation.x += 0.01
+// cube.rotation.y += 0.01
+ renderer.render(scene, camera)
+}
+animate()
+
+// let animation
+
+window.animate = () => {
+ // new Tween.Tween(group.position).to({ y: group.position.y - 1}, 1000 * 0.35/2).yoyo(true).repeat(1).start()
+ new tweenJs.Tween(group.rotation, tweenGroup).to({ z: THREE.MathUtils.degToRad(90) }, 1000 * 0.35 / 2).yoyo(true).repeat(Infinity).start().onRepeat(() => {
+ console.log('done')
+ })
+}
+
+window.stop = () => {
+ tweenGroup.removeAll()
+}
+
+
+function createGeometryFromImage() {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABEElEQVQ4jWNkIAPw2Zv9J0cfXPOSvx/+L/n74T+HqsJ/JlI1T9u3i6H91B7ybdY+vgZuO1majV+fppFmPnuz/+ihy2dv9t/49Wm8mlECkV1FHh5FfPZm/1XXTGX4cechA4eKPMNVq1CGH7cfMBJ0rlxX+X8OVYX/xq9P/5frKifoZ0Z0AwS8HRkYGBgYvt+8xyDXUUbQZgwJPnuz/+wq8gw/7zxk+PXsFUFno0h6mon+l5fgZFhwnYmBTUqMgYGBgaAhLMiaHQyFGOZvf8Lw49FXRgYGhv8MDAwwg/7jMoQFFury/C8Y5m9/wnADohnZVryJhoWBARJ9Cw69gtmMAgiFAcuvZ68Yfj17hU8NXgAATdKfkzbQhBEAAAAASUVORK5CYII='
+ console.log('img.complete', img.complete)
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = img.width;
+ canvas.height = img.height;
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0, img.width, img.height);
+ const imgData = context.getImageData(0, 0, img.width, img.height);
+
+ const shape = new THREE.Shape();
+ for (let y = 0; y < img.height; y++) {
+ for (let x = 0; x < img.width; x++) {
+ const index = (y * img.width + x) * 4;
+ const alpha = imgData.data[index + 3];
+ if (alpha !== 0) {
+ shape.lineTo(x, y);
+ }
+ }
+ }
+
+ const geometry = new THREE.ShapeGeometry(shape);
+ resolve(geometry);
+ };
+ img.onerror = reject;
+ });
+}
+
+// Usage:
+const shapeGeomtry = createGeometryFromImage().then(geometry => {
+ const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
+ const mesh = new THREE.Mesh(geometry, material);
+ scene.add(mesh);
+})
diff --git a/prismarine-viewer/viewer/lib/entities.js b/prismarine-viewer/viewer/lib/entities.js
index 57a45a444..01ce7d6d3 100644
--- a/prismarine-viewer/viewer/lib/entities.js
+++ b/prismarine-viewer/viewer/lib/entities.js
@@ -282,6 +282,46 @@ export class Entities extends EventEmitter {
}
}
+ getItemMesh(item) {
+ const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
+ if (textureUv) {
+ // todo use geometry buffer uv instead!
+ const { u, v, size, su, sv, texture } = textureUv
+ const itemsTexture = texture.clone()
+ itemsTexture.flipY = true
+ itemsTexture.offset.set(u, 1 - v - (sv ?? size))
+ itemsTexture.repeat.set(su ?? size, sv ?? size)
+ itemsTexture.needsUpdate = true
+ itemsTexture.magFilter = THREE.NearestFilter
+ itemsTexture.minFilter = THREE.NearestFilter
+ const itemsTextureFlipped = itemsTexture.clone()
+ itemsTextureFlipped.repeat.x *= -1
+ itemsTextureFlipped.needsUpdate = true
+ itemsTextureFlipped.offset.set(u + (su ?? size), 1 - v - (sv ?? size))
+ const material = new THREE.MeshStandardMaterial({
+ map: itemsTexture,
+ transparent: true,
+ alphaTest: 0.1,
+ })
+ const materialFlipped = new THREE.MeshStandardMaterial({
+ map: itemsTextureFlipped,
+ transparent: true,
+ alphaTest: 0.1,
+ })
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
+ // top left and right bottom are black box materials others are transparent
+ new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
+ new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
+ material, materialFlipped,
+ ])
+ return {
+ mesh,
+ itemsTexture,
+ itemsTextureFlipped,
+ }
+ }
+ }
+
update(/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
let isPlayerModel = entity.name === 'player'
if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
@@ -296,52 +336,23 @@ export class Entities extends EventEmitter {
//@ts-expect-error
const item = entity.metadata?.find(m => typeof m === 'object' && m?.itemCount)
if (item) {
- const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
- if (textureUv) {
- // todo use geometry buffer uv instead!
- const { u, v, size, su, sv, texture } = textureUv
- const itemsTexture = texture.clone()
- itemsTexture.flipY = true
- itemsTexture.offset.set(u, 1 - v - (sv ?? size))
- itemsTexture.repeat.set(su ?? size, sv ?? size)
- itemsTexture.needsUpdate = true
- itemsTexture.magFilter = THREE.NearestFilter
- itemsTexture.minFilter = THREE.NearestFilter
- const itemsTextureFlipped = itemsTexture.clone()
- itemsTextureFlipped.repeat.x *= -1
- itemsTextureFlipped.needsUpdate = true
- itemsTextureFlipped.offset.set(u + (su ?? size), 1 - v - (sv ?? size))
- const material = new THREE.MeshStandardMaterial({
- map: itemsTexture,
- transparent: true,
- alphaTest: 0.1,
- })
- const materialFlipped = new THREE.MeshStandardMaterial({
- map: itemsTextureFlipped,
- transparent: true,
- alphaTest: 0.1,
- })
- mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
- // top left and right bottom are black box materials others are transparent
- new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
- new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
- material, materialFlipped,
- ])
- mesh.scale.set(0.5, 0.5, 0.5)
- mesh.position.set(0, 0.2, 0)
+ const object = this.getItemMesh(item)
+ if (object) {
+ object.scale.set(0.5, 0.5, 0.5)
+ object.position.set(0, 0.2, 0)
// set faces
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
// viewer.scene.add(mesh)
const clock = new THREE.Clock()
- mesh.onBeforeRender = () => {
+ object.onBeforeRender = () => {
const delta = clock.getDelta()
- mesh.rotation.y += delta
+ object.rotation.y += delta
}
//@ts-expect-error
group.additionalCleanup = () => {
// important: avoid texture memory leak and gpu slowdown
- itemsTexture.dispose()
- itemsTextureFlipped.dispose()
+ object.itemsTexture.dispose()
+ object.itemsTextureFlipped.dispose()
}
}
}
diff --git a/prismarine-viewer/viewer/lib/holdingBlock.ts b/prismarine-viewer/viewer/lib/holdingBlock.ts
new file mode 100644
index 000000000..f5a0ca799
--- /dev/null
+++ b/prismarine-viewer/viewer/lib/holdingBlock.ts
@@ -0,0 +1,207 @@
+import * as THREE from 'three'
+import * as tweenJs from '@tweenjs/tween.js'
+import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
+import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
+
+export type HandItemBlock = {
+ name
+ properties
+}
+
+export default class HoldingBlock {
+ holdingBlock: THREE.Object3D | undefined = undefined
+ swingAnimation: tweenJs.Group | undefined = undefined
+ blockSwapAnimation: {
+ tween: tweenJs.Group
+ hidden: boolean
+ } | undefined = undefined
+ cameraGroup = new THREE.Mesh()
+ objectOuterGroup = new THREE.Group()
+ objectInnerGroup = new THREE.Group()
+ camera: THREE.Group | THREE.PerspectiveCamera
+ stopUpdate = false
+ lastHeldItem: HandItemBlock | undefined
+ toBeRenderedItem: HandItemBlock | undefined
+ isSwinging = false
+ nextIterStopCallbacks: Array<() => void> | undefined
+
+ constructor (public scene: THREE.Scene) {
+ this.initCameraGroup()
+ }
+
+ initCameraGroup () {
+ this.cameraGroup = new THREE.Mesh()
+ this.scene.add(this.cameraGroup)
+ }
+
+ startSwing () {
+ this.nextIterStopCallbacks = undefined // forget about cancelling
+ if (this.isSwinging) return
+ this.swingAnimation = new tweenJs.Group()
+ this.isSwinging = true
+ const cube = this.cameraGroup.children[0]
+ if (cube) {
+ // const DURATION = 1000 * 0.35 / 2
+ const DURATION = 1000 * 0.35 / 3
+ // const DURATION = 1000
+ const initialPos = {
+ x: this.objectInnerGroup.position.x,
+ y: this.objectInnerGroup.position.y,
+ z: this.objectInnerGroup.position.z
+ }
+ const initialRot = {
+ x: this.objectInnerGroup.rotation.x,
+ y: this.objectInnerGroup.rotation.y,
+ z: this.objectInnerGroup.rotation.z
+ }
+ const mainAnim = new tweenJs.Tween(this.objectInnerGroup.position, this.swingAnimation).to({ y: this.objectInnerGroup.position.y - this.objectInnerGroup.scale.y / 2 }, DURATION).yoyo(true).repeat(Infinity).start()
+ let i = 0
+ mainAnim.onRepeat(() => {
+ i++
+ if (this.nextIterStopCallbacks && i % 2 === 0) {
+ for (const callback of this.nextIterStopCallbacks) {
+ callback()
+ }
+ this.nextIterStopCallbacks = undefined
+ this.isSwinging = false
+ this.swingAnimation!.removeAll()
+ this.swingAnimation = undefined
+ // todo refactor to be more generic for animations
+ this.objectInnerGroup.position.set(initialPos.x, initialPos.y, initialPos.z)
+ // this.objectInnerGroup.rotation.set(initialRot.x, initialRot.y, initialRot.z)
+ Object.assign(this.objectInnerGroup.rotation, initialRot)
+ }
+ })
+
+ new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ z: THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
+ new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ x: -THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
+ }
+ }
+
+ async stopSwing () {
+ if (!this.isSwinging) return
+ // might never resolve!
+ /* return */void new Promise((resolve) => {
+ this.nextIterStopCallbacks ??= []
+ this.nextIterStopCallbacks.push(() => {
+ resolve()
+ })
+ })
+ }
+
+ update (camera: typeof this.camera) {
+ this.camera = camera
+ this.swingAnimation?.update()
+ this.blockSwapAnimation?.tween.update()
+ this.updateCameraGroup()
+ }
+
+ // worldTest () {
+ // const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
+ // mesh.position.set(0.5, 0.5, 0.5)
+ // const group = new THREE.Group()
+ // group.add(mesh)
+ // group.position.set(-0.5, -0.5, -0.5)
+ // const outerGroup = new THREE.Group()
+ // outerGroup.add(group)
+ // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
+ // this.scene.add(outerGroup)
+
+ // new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
+ // }
+
+ async playBlockSwapAnimation () {
+ // if (this.blockSwapAnimation) return
+ this.blockSwapAnimation ??= {
+ tween: new tweenJs.Group(),
+ hidden: false
+ }
+ const DURATION = 1000 * 0.35 / 2
+ const tween = new tweenJs.Tween(this.objectInnerGroup.position, this.blockSwapAnimation.tween).to({
+ y: this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (this.blockSwapAnimation.hidden ? 1 : -1))
+ }, DURATION).start()
+ return new Promise((resolve) => {
+ tween.onComplete(() => {
+ if (this.blockSwapAnimation!.hidden) {
+ this.blockSwapAnimation = undefined
+ } else {
+ this.blockSwapAnimation!.hidden = !this.blockSwapAnimation!.hidden
+ }
+ resolve()
+ })
+ })
+ }
+
+ isDifferentItem (block: HandItemBlock | undefined) {
+ return this.lastHeldItem && (this.lastHeldItem.name !== block?.name || JSON.stringify(this.lastHeldItem.properties) !== JSON.stringify(block?.properties ?? '{}'))
+ }
+
+ updateCameraGroup () {
+ if (this.stopUpdate) return
+ const { camera } = this
+ this.cameraGroup.position.copy(camera.position)
+ this.cameraGroup.rotation.copy(camera.rotation)
+
+ const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
+ // const x = window.x ?? 0.25 * viewerSize.width / viewerSize.height
+ // const x = 0 * viewerSize.width / viewerSize.height
+ const x = 0.2 * viewerSize.width / viewerSize.height
+ this.objectOuterGroup.position.set(x, -0.3, -0.45)
+ }
+
+ async initHandObject (material: THREE.Material, blockstatesModels: any, blocksAtlases: any, block?: HandItemBlock) {
+ let animatingCurrent = false
+ if (!this.swingAnimation && !this.blockSwapAnimation && this.isDifferentItem(block)) {
+ animatingCurrent = true
+ await this.playBlockSwapAnimation()
+ this.holdingBlock?.removeFromParent()
+ this.holdingBlock = undefined
+ }
+ this.lastHeldItem = block
+ if (!block) {
+ this.holdingBlock?.removeFromParent()
+ this.holdingBlock = undefined
+ this.swingAnimation = undefined
+ this.blockSwapAnimation = undefined
+ return
+ }
+ const blockProvider = worldBlockProvider(blockstatesModels, blocksAtlases, 'latest')
+ const models = blockProvider.getAllResolvedModels0_1(block, true)
+ const blockInner = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
+ // const { mesh: itemMesh } = viewer.entities.getItemMesh({
+ // itemId: 541,
+ // })!
+ // itemMesh.position.set(0.5, 0.5, 0.5)
+ // const blockInner = itemMesh
+ blockInner.name = 'holdingBlock'
+ const blockOuterGroup = new THREE.Group()
+ blockOuterGroup.add(blockInner)
+ this.holdingBlock = blockInner
+ this.objectInnerGroup = new THREE.Group()
+ this.objectInnerGroup.add(blockOuterGroup)
+ this.objectInnerGroup.position.set(-0.5, -0.5, -0.5)
+ // todo cleanup
+ if (animatingCurrent) {
+ this.objectInnerGroup.position.y -= this.objectInnerGroup.scale.y * 1.5
+ }
+ Object.assign(blockOuterGroup.position, { x: 0.5, y: 0.5, z: 0.5 })
+
+ this.objectOuterGroup = new THREE.Group()
+ this.objectOuterGroup.add(this.objectInnerGroup)
+
+ this.cameraGroup.add(this.objectOuterGroup)
+ const rotation = -45 + -90
+ // const rotation = -45 // should be for item
+ this.holdingBlock.rotation.set(0, THREE.MathUtils.degToRad(rotation), 0, 'ZYX')
+
+ // const scale = window.scale ?? 0.2
+ const scale = 0.2
+ this.objectOuterGroup.scale.set(scale, scale, scale)
+ // this.objectOuterGroup.position.set(x, window.y ?? -0.41, window.z ?? -0.45)
+ // this.objectOuterGroup.position.set(x, 0, -0.45)
+
+ if (animatingCurrent) {
+ await this.playBlockSwapAnimation()
+ }
+ }
+}
diff --git a/prismarine-viewer/viewer/lib/mesher/standaloneRenderer.ts b/prismarine-viewer/viewer/lib/mesher/standaloneRenderer.ts
index 2dc2f5994..43369cc21 100644
--- a/prismarine-viewer/viewer/lib/mesher/standaloneRenderer.ts
+++ b/prismarine-viewer/viewer/lib/mesher/standaloneRenderer.ts
@@ -272,3 +272,19 @@ export const renderBlockThree = (...args: Parameters) => {
+ const geometry = renderBlockThree(...args)
+ const mesh = new THREE.Mesh(geometry, material)
+ mesh.position.set(-0.5, -0.5, -0.5)
+ const group = new THREE.Group()
+ group.add(mesh)
+ group.rotation.set(0, -THREE.MathUtils.degToRad(90), 0, 'ZYX')
+ globalThis.mesh = group
+ return group
+ // return new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
+}
+
+export const setBlockPosition = (object: THREE.Object3D, position: { x: number, y: number, z: number }) => {
+ object.position.set(position.x + 0.5, position.y + 0.5, position.z + 0.5)
+}
diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts
index 7893bccce..7cd759e40 100644
--- a/prismarine-viewer/viewer/lib/viewer.ts
+++ b/prismarine-viewer/viewer/lib/viewer.ts
@@ -7,7 +7,7 @@ import { Entities } from './entities'
import { Primitives } from './primitives'
import { WorldRendererThree } from './worldrendererThree'
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
-import { renderBlockThree } from './mesher/standaloneRenderer'
+import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
export class Viewer {
scene: THREE.Scene
@@ -101,18 +101,32 @@ export class Viewer {
}
demoModel () {
+ //@ts-expect-error
+ const pos = cursorBlockRel(0, 1, 0).position
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlases, 'latest')
const models = blockProvider.getAllResolvedModels0_1({
- name: 'item_frame',
+ name: 'furnace',
properties: {
- map: false
+ // map: false
}
- })
- const geometry = renderBlockThree(models, undefined, 'plains', loadedData)
+ }, true)
const { material } = this.world
- // block material
- const mesh = new THREE.Mesh(geometry, material)
- mesh.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
+ const mesh = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
+ // mesh.rotation.y = THREE.MathUtils.degToRad(90)
+ setBlockPosition(mesh, pos)
+ const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
+ mesh.add(helper)
+ this.scene.add(mesh)
+ }
+
+ demoItem () {
+ //@ts-expect-error
+ const pos = cursorBlockRel(0, 1, 0).position
+ const { mesh } = this.entities.getItemMesh({
+ itemId: 541,
+ })!
+ mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
+ // mesh.scale.set(0.5, 0.5, 0.5)
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
mesh.add(helper)
this.scene.add(mesh)
diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts
index 381526d93..d832d3db2 100644
--- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts
+++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts
@@ -5,6 +5,7 @@ import { EventEmitter } from 'events'
import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
import { Vec3 } from 'vec3'
import { BotEvents } from 'mineflayer'
+import { getItemFromBlock } from '../../../src/botUtils'
import { chunkPos } from './simpleUtils'
export type ChunkPosKey = string
@@ -20,6 +21,14 @@ export class WorldDataEmitter extends EventEmitter {
private eventListeners: Record = {}
private readonly emitter: WorldDataEmitter
keepChunksDistance = 0
+ _handDisplay = false
+ get handDisplay () {
+ return this._handDisplay
+ }
+ set handDisplay (newVal) {
+ this._handDisplay = newVal
+ this.eventListeners.heldItemChanged?.()
+ }
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
super()
@@ -55,7 +64,7 @@ export class WorldDataEmitter extends EventEmitter {
})
}
- this.eventListeners[bot.username] = {
+ this.eventListeners = {
// 'move': botPosition,
entitySpawn (e: any) {
emitEntity(e)
@@ -70,7 +79,7 @@ export class WorldDataEmitter extends EventEmitter {
this.emitter.emit('entity', { id: e.id, delete: true })
},
chunkColumnLoad: (pos: Vec3) => {
- this.loadChunk(pos)
+ void this.loadChunk(pos)
},
chunkColumnUnload: (pos: Vec3) => {
this.unloadChunk(pos)
@@ -82,7 +91,24 @@ export class WorldDataEmitter extends EventEmitter {
time: () => {
this.emitter.emit('time', bot.time.timeOfDay)
},
+ heldItemChanged: () => {
+ if (!this.handDisplay) {
+ viewer.world.onHandItemSwitch(undefined)
+ return
+ }
+ const newItem = bot.heldItem
+ if (!newItem) {
+ viewer.world.onHandItemSwitch(undefined)
+ return
+ }
+ const block = loadedData.blocksByName[newItem.name]
+ // todo clean types
+ const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
+ viewer.world.onHandItemSwitch({ name: newItem.name, properties: blockProperties })
+ },
} satisfies Partial
+ this.eventListeners.heldItemChanged()
+
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16)
@@ -105,7 +131,7 @@ export class WorldDataEmitter extends EventEmitter {
this.emitter.emit('listening')
}
- for (const [evt, listener] of Object.entries(this.eventListeners[bot.username])) {
+ for (const [evt, listener] of Object.entries(this.eventListeners)) {
bot.on(evt as any, listener)
}
@@ -116,10 +142,9 @@ export class WorldDataEmitter extends EventEmitter {
}
removeListenersFromBot (bot: import('mineflayer').Bot) {
- for (const [evt, listener] of Object.entries(this.eventListeners[bot.username])) {
+ for (const [evt, listener] of Object.entries(this.eventListeners)) {
bot.removeListener(evt as any, listener)
}
- delete this.eventListeners[bot.username]
}
async init (pos: Vec3) {
diff --git a/prismarine-viewer/viewer/lib/worldrendererCommon.ts b/prismarine-viewer/viewer/lib/worldrendererCommon.ts
index 58e5a6f50..482907d4e 100644
--- a/prismarine-viewer/viewer/lib/worldrendererCommon.ts
+++ b/prismarine-viewer/viewer/lib/worldrendererCommon.ts
@@ -16,6 +16,7 @@ import { toMajorVersion } from '../../../src/utils'
import { buildCleanupDecorator } from './cleanupDecorator'
import { defaultMesherConfig } from './mesher/shared'
import { chunkPos } from './simpleUtils'
+import { HandItemBlock } from './holdingBlock'
function mod (x, n) {
return ((x % n) + n) % n
@@ -37,6 +38,7 @@ type CustomTexturesData = {
export abstract class WorldRendererCommon {
worldConfig = { minY: 0, worldHeight: 256 }
+ // todo need to cleanup
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
@worldCleanup()
@@ -59,6 +61,7 @@ export abstract class WorldRendererCommon
textureDownloaded (): void
}>
customTexturesDataUrl = undefined as string | undefined
+ @worldCleanup()
currentTextureImage = undefined as any
workers: any[] = []
viewerPosition?: Vec3
@@ -157,6 +160,9 @@ export abstract class WorldRendererCommon
}
}
+ onHandItemSwitch (item: HandItemBlock | undefined): void { }
+ changeHandSwingingState (isAnimationPlaying: boolean): void { }
+
abstract handleWorkerMessage (data: WorkerReceive): void
abstract updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void
diff --git a/prismarine-viewer/viewer/lib/worldrendererThree.ts b/prismarine-viewer/viewer/lib/worldrendererThree.ts
index 6c14e2439..b1644ebd2 100644
--- a/prismarine-viewer/viewer/lib/worldrendererThree.ts
+++ b/prismarine-viewer/viewer/lib/worldrendererThree.ts
@@ -9,7 +9,7 @@ import { renderSign } from '../sign-renderer'
import { chunkPos, sectionPos } from './simpleUtils'
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
import { disposeObject } from './threeJsUtils'
-import { renderBlockThree } from './mesher/standaloneRenderer'
+import HoldingBlock, { HandItemBlock } from './holdingBlock'
export class WorldRendererThree extends WorldRendererCommon {
outputFormat = 'threeJs' as const
@@ -19,7 +19,7 @@ export class WorldRendererThree extends WorldRendererCommon {
signsCache = new Map()
starField: StarField
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
- cameraGroup = new THREE.Group()
+ holdingBlock: HoldingBlock
get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
@@ -28,8 +28,34 @@ export class WorldRendererThree extends WorldRendererCommon {
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
super(config)
this.starField = new StarField(scene)
- // this.initCameraGroup()
- // this.initHandObject()
+ this.holdingBlock = new HoldingBlock(this.scene)
+ this.onHandItemSwitch({
+ name: 'furnace',
+ properties: {}
+ })
+
+ this.renderUpdateEmitter.on('textureDownloaded', () => {
+ if (this.holdingBlock.toBeRenderedItem) {
+ this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem)
+ this.holdingBlock.toBeRenderedItem = undefined
+ }
+ })
+ }
+
+ onHandItemSwitch (item: HandItemBlock | undefined) {
+ if (!this.currentTextureImage) {
+ this.holdingBlock.toBeRenderedItem = item
+ return
+ }
+ void this.holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item)
+ }
+
+ changeHandSwingingState (isAnimationPlaying: boolean) {
+ if (isAnimationPlaying) {
+ this.holdingBlock.startSwing()
+ } else {
+ void this.holdingBlock.stopSwing()
+ }
}
timeUpdated (newTime: number): void {
@@ -173,6 +199,7 @@ export class WorldRendererThree extends WorldRendererCommon {
render () {
tweenJs.update()
+ this.holdingBlock.update(this.camera)
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
this.renderer.render(this.scene, cam)
diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts
index 64e589f99..e5964646e 100644
--- a/src/inventoryWindows.ts
+++ b/src/inventoryWindows.ts
@@ -87,8 +87,13 @@ export const onGameLoad = (onLoad) => {
return
}
const craftingSlots = bot.inventory.slots.slice(1, 5)
- const resultingItem = getResultingRecipe(craftingSlots, 2)
- void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null)
+ try {
+ const resultingItem = getResultingRecipe(craftingSlots, 2)
+ void bot.creative.setInventorySlot(craftingResultSlot, resultingItem ?? null)
+ } catch (err) {
+ console.error(err)
+ // todo resolve the error! and why would we ever get here on every update?
+ }
}) as any)
bot.on('windowClose', () => {
diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx
index 644ec66cb..2082acdb0 100644
--- a/src/optionsGuiScheme.tsx
+++ b/src/optionsGuiScheme.tsx
@@ -88,6 +88,7 @@ export const guiOptionsScheme: {
unit: '',
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
},
+ handDisplay: {},
},
],
main: [
diff --git a/src/optionsStorage.ts b/src/optionsStorage.ts
index cac8c9f8c..b7bf4abed 100644
--- a/src/optionsStorage.ts
+++ b/src/optionsStorage.ts
@@ -47,6 +47,7 @@ const defaultOptions = {
enabledResourcepack: null as string | null,
useVersionsTextures: 'latest',
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
+ handDisplay: false,
// antiAliasing: false,
diff --git a/src/topRightStats.ts b/src/topRightStats.ts
index 71303e81a..f0462ae61 100644
--- a/src/topRightStats.ts
+++ b/src/topRightStats.ts
@@ -75,3 +75,14 @@ export const statsEnd = () => {
stats2.end()
statsGl.end()
}
+
+window.statsPerSec = {}
+let statsPerSec = {}
+window.addStatPerSec = (name) => {
+ statsPerSec[name] ??= 0
+ statsPerSec[name]++
+}
+setInterval(() => {
+ window.statsPerSec = statsPerSec
+ statsPerSec = {}
+}, 1000)
diff --git a/src/watchOptions.ts b/src/watchOptions.ts
index 9deb46a57..d26d2b90d 100644
--- a/src/watchOptions.ts
+++ b/src/watchOptions.ts
@@ -66,11 +66,11 @@ export const watchOptionsAfterViewerInit = () => {
let viewWatched = false
export const watchOptionsAfterWorldViewInit = () => {
- worldView!.keepChunksDistance = options.keepChunksDistance
if (viewWatched) return
viewWatched = true
watchValue(options, o => {
if (!worldView) return
worldView.keepChunksDistance = o.keepChunksDistance
+ worldView.handDisplay = o.handDisplay
})
}
diff --git a/src/worldInteractions.ts b/src/worldInteractions.ts
index bc54efcda..cef65b96a 100644
--- a/src/worldInteractions.ts
+++ b/src/worldInteractions.ts
@@ -294,6 +294,8 @@ class WorldInteraction {
bot.lookAt = oldLookAt
}).catch(console.warn)
}
+ viewer.world.changeHandSwingingState(true)
+ viewer.world.changeHandSwingingState(false)
} else if (!stop) {
const offhand = activate ? false : activatableItems(bot.inventory.slots[45]?.name ?? '')
bot.activateItem(offhand) // todo offhand
@@ -351,11 +353,15 @@ class WorldInteraction {
})
customEvents.emit('digStart')
this.lastDigged = Date.now()
+ viewer.world.changeHandSwingingState(true)
} 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)
+ }
this.prevOnGround = onGround
// Show cursor