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