feat: support fetching skins by username for players! Use popular skinview3 for rendering players, so things like ears should render propertly
feat: Add support for head rotation. feat: add `window.cursorEntity` for inspecting or watching entity props you're looking at fix: fix entity in playground
This commit is contained in:
parent
8d05b410d2
commit
86a4ac68f5
13 changed files with 333 additions and 87 deletions
|
|
@ -23,7 +23,7 @@ const buildOptions = {
|
|||
entryPoints: [path.join(__dirname, './viewer/lib/worker.js')],
|
||||
minify: true,
|
||||
logLevel: 'info',
|
||||
drop: watch ? [
|
||||
drop: !watch ? [
|
||||
'debugger'
|
||||
] : [],
|
||||
sourcemap: 'linked',
|
||||
|
|
@ -121,7 +121,7 @@ const buildOptions = {
|
|||
],
|
||||
}
|
||||
|
||||
if (process.argv.includes('-w')) {
|
||||
if (watch) {
|
||||
const ctx = await context(buildOptions)
|
||||
await ctx.watch()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ const buildOptions = {
|
|||
},
|
||||
inject: [],
|
||||
metafile: true,
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'minecraft-data',
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { GUI } from 'lil-gui'
|
|||
import { toMajor } from '../viewer/lib/version'
|
||||
import { loadScript } from '../viewer/lib/utils'
|
||||
import JSZip from 'jszip'
|
||||
import { TWEEN_DURATION } from '../viewer/lib/entities'
|
||||
import Entity from '../viewer/lib/entity/Entity'
|
||||
|
||||
globalThis.THREE = THREE
|
||||
//@ts-ignore
|
||||
|
|
@ -82,14 +84,15 @@ async function main () {
|
|||
gui.add(params, 'block', mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b)))
|
||||
const metadataGui = gui.add(params, 'metadata')
|
||||
gui.add(params, 'supportBlock')
|
||||
gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name)).listen()
|
||||
gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b))).listen()
|
||||
gui.add(params, 'removeEntity')
|
||||
gui.add(params, 'entityRotate')
|
||||
gui.add(params, 'skip')
|
||||
gui.add(params, 'playSound')
|
||||
gui.add(params, 'blockIsomorphicRenderBundle')
|
||||
gui.open(false)
|
||||
let folder = gui.addFolder('metadata')
|
||||
let metadataFolder = gui.addFolder('metadata')
|
||||
let entityRotationFolder = gui.addFolder('entity rotation')
|
||||
|
||||
const Chunk = ChunkLoader(version)
|
||||
const Block = BlockLoader(version)
|
||||
|
|
@ -293,19 +296,44 @@ async function main () {
|
|||
controls.update()
|
||||
|
||||
let blockProps = {}
|
||||
let entityOverrides = {}
|
||||
const getBlock = () => {
|
||||
return mcData.blocksByName[params.block || 'air']
|
||||
}
|
||||
|
||||
const entityUpdateShared = () => {
|
||||
viewer.entities.clear()
|
||||
if (!params.entity) return
|
||||
worldView.emit('entity', {
|
||||
id: 'id', name: params.entity, pos: targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: 'username', yaw: Math.PI, pitch: 0
|
||||
})
|
||||
const enableSkeletonDebug = (obj) => {
|
||||
const {children, isSkeletonHelper} = obj
|
||||
if (!Array.isArray(children)) return
|
||||
if (isSkeletonHelper) {
|
||||
obj.visible = true
|
||||
return
|
||||
}
|
||||
for (const child of children) {
|
||||
if (typeof child === 'object') enableSkeletonDebug(child)
|
||||
}
|
||||
}
|
||||
enableSkeletonDebug(viewer.entities.entities['id'])
|
||||
setTimeout(() => {
|
||||
viewer.update()
|
||||
viewer.render()
|
||||
}, TWEEN_DURATION)
|
||||
}
|
||||
|
||||
const onUpdate = {
|
||||
block () {
|
||||
folder.destroy()
|
||||
metadataFolder.destroy()
|
||||
const block = mcData.blocksByName[params.block]
|
||||
if (!block) return
|
||||
const props = new Block(block.id, 0, 0).getProperties()
|
||||
//@ts-ignore
|
||||
const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
|
||||
folder = gui.addFolder('metadata')
|
||||
metadataFolder = gui.addFolder('metadata')
|
||||
if (states) {
|
||||
for (const state of states) {
|
||||
let defaultValue
|
||||
|
|
@ -328,25 +356,31 @@ async function main () {
|
|||
}
|
||||
blockProps[state.name] = defaultValue
|
||||
if (state.type === 'enum') {
|
||||
folder.add(blockProps, state.name, state.values)
|
||||
metadataFolder.add(blockProps, state.name, state.values)
|
||||
} else {
|
||||
folder.add(blockProps, state.name)
|
||||
metadataFolder.add(blockProps, state.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [name, value] of Object.entries(props)) {
|
||||
blockProps[name] = value
|
||||
folder.add(blockProps, name)
|
||||
metadataFolder.add(blockProps, name)
|
||||
}
|
||||
}
|
||||
folder.open()
|
||||
metadataFolder.open()
|
||||
},
|
||||
entity () {
|
||||
viewer.entities.clear()
|
||||
entityUpdateShared()
|
||||
if (!params.entity) return
|
||||
worldView.emit('entity', {
|
||||
id: 'id', name: params.entity, pos: targetPos.offset(0, 1, 0), width: 1, height: 1, username: 'username'
|
||||
})
|
||||
|
||||
Entity.getStaticData(params.entity)
|
||||
entityRotationFolder.destroy()
|
||||
entityRotationFolder = gui.addFolder('entity rotation')
|
||||
entityRotationFolder.add(params, 'entityRotate')
|
||||
entityRotationFolder.open()
|
||||
},
|
||||
supportBlock () {
|
||||
viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -357,7 +391,7 @@ async function main () {
|
|||
if (metadataUpdate) {
|
||||
block = new Block(blockId, 0, params.metadata)
|
||||
Object.assign(blockProps, block.getProperties())
|
||||
for (const _child of folder.children) {
|
||||
for (const _child of metadataFolder.children) {
|
||||
const child = _child as import('lil-gui').Controller
|
||||
child.updateDisplay()
|
||||
}
|
||||
|
|
@ -373,18 +407,21 @@ async function main () {
|
|||
|
||||
//@ts-ignore
|
||||
viewer.setBlockStateId(targetPos, block.stateId)
|
||||
console.log('up', block.stateId)
|
||||
console.log('up stateId', block.stateId)
|
||||
params.metadata = block.metadata
|
||||
metadataGui.updateDisplay()
|
||||
// viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
|
||||
if (!skipQs) {
|
||||
setQs()
|
||||
}
|
||||
}
|
||||
gui.onChange(({ property }) => {
|
||||
if (property === 'camera') return
|
||||
onUpdate[property]?.()
|
||||
applyChanges(property === 'metadata')
|
||||
gui.onChange(({ property, object }) => {
|
||||
if (object === params) {
|
||||
if (property === 'camera') return
|
||||
onUpdate[property]?.()
|
||||
applyChanges(property === 'metadata')
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
})
|
||||
viewer.waitForChunksToRender().then(async () => {
|
||||
await new Promise(resolve => {
|
||||
|
|
@ -395,12 +432,8 @@ async function main () {
|
|||
}
|
||||
applyChanges(true)
|
||||
gui.openAnimated()
|
||||
// worldView.emit('entity', {
|
||||
// id: 'id', name: 'player', pos: targetPos.offset(1, -2, 0), width: 1, height: 1, username: 'username'
|
||||
// })
|
||||
})
|
||||
|
||||
// Browser animation loop
|
||||
const animate = () => {
|
||||
// if (controls) controls.update()
|
||||
// worldView.updatePosition(controls.target)
|
||||
|
|
@ -455,11 +488,5 @@ async function main () {
|
|||
params.playSound()
|
||||
}
|
||||
}, { capture: true })
|
||||
|
||||
setTimeout(() => {
|
||||
// worldView.emit('entity', {
|
||||
// id: 'id', name: 'player', pos: center.offset(1, -2, 0), width: 1, height: 1, username: 'username'
|
||||
// })
|
||||
}, 1500)
|
||||
}
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<style type="text/css">
|
||||
html {
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
}
|
||||
|
||||
html, body {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
//@ts-check
|
||||
const THREE = require('three')
|
||||
const TWEEN = require('@tweenjs/tween.js')
|
||||
|
||||
const Entity = require('./entity/Entity')
|
||||
const { dispose3 } = require('./dispose')
|
||||
const EventEmitter = require('events')
|
||||
import { PlayerObject } from 'skinview3d'
|
||||
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
||||
// todo replace with url
|
||||
import stevePng from 'minecraft-assets/minecraft-assets/data/1.20.2/entity/player/wide/steve.png'
|
||||
|
||||
function getUsernameTexture(username, { fontFamily = 'sans-serif' }) {
|
||||
export const TWEEN_DURATION = 50 // todo should be 100
|
||||
|
||||
function getUsernameTexture (username, { fontFamily = 'sans-serif' }) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get 2d context')
|
||||
|
||||
const fontSize = 50
|
||||
const padding = 5
|
||||
|
|
@ -27,11 +36,12 @@ function getUsernameTexture(username, { fontFamily = 'sans-serif' }) {
|
|||
return canvas
|
||||
}
|
||||
|
||||
function getEntityMesh (entity, scene, options) {
|
||||
function getEntityMesh (entity, scene, options, overrides) {
|
||||
if (entity.name) {
|
||||
try {
|
||||
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
||||
const e = new Entity('1.16.4', entity.name.toLowerCase(), scene)
|
||||
const entityName = entity.name.toLowerCase()
|
||||
const e = new Entity('1.16.4', entityName, scene, overrides)
|
||||
|
||||
if (entity.username !== undefined) {
|
||||
const canvas = getUsernameTexture(entity.username, options)
|
||||
|
|
@ -58,8 +68,9 @@ function getEntityMesh (entity, scene, options) {
|
|||
return cube
|
||||
}
|
||||
|
||||
class Entities {
|
||||
export class Entities extends EventEmitter {
|
||||
constructor (scene) {
|
||||
super()
|
||||
this.scene = scene
|
||||
this.entities = {}
|
||||
this.entitiesOptions = {}
|
||||
|
|
@ -73,31 +84,132 @@ class Entities {
|
|||
this.entities = {}
|
||||
}
|
||||
|
||||
update (entity) {
|
||||
updatePlayerSkin (entityId, skinUrl, capeUrl = undefined) {
|
||||
const getPlayerObject = () => {
|
||||
/** @type {PlayerObject} */
|
||||
return this.entities[entityId]?.playerObject
|
||||
}
|
||||
let playerObject = getPlayerObject()
|
||||
if (!playerObject) return
|
||||
loadImage(skinUrl).then((image) => {
|
||||
playerObject = getPlayerObject()
|
||||
if (!playerObject) return
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(skinCanvas, image)
|
||||
const skinTexture = new THREE.CanvasTexture(skinCanvas)
|
||||
skinTexture.magFilter = THREE.NearestFilter
|
||||
skinTexture.minFilter = THREE.NearestFilter
|
||||
skinTexture.needsUpdate = true
|
||||
//@ts-ignore
|
||||
playerObject.skin.map = skinTexture
|
||||
playerObject.skin.modelType = inferModelType(skinCanvas)
|
||||
|
||||
const earsCanvas = document.createElement('canvas')
|
||||
loadEarsToCanvasFromSkin(earsCanvas, skinCanvas)
|
||||
const earsTexture = new THREE.CanvasTexture(earsCanvas)
|
||||
earsTexture.magFilter = THREE.NearestFilter
|
||||
earsTexture.minFilter = THREE.NearestFilter
|
||||
earsTexture.needsUpdate = true
|
||||
//@ts-ignore
|
||||
playerObject.ears.map = earsTexture
|
||||
})
|
||||
|
||||
if (capeUrl) {
|
||||
loadImage(capeUrl).then((capeImage) => {
|
||||
playerObject = getPlayerObject()
|
||||
if (!playerObject) return
|
||||
const capeCanvas = document.createElement('canvas')
|
||||
loadCapeToCanvas(capeCanvas, capeImage)
|
||||
|
||||
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||
capeTexture.magFilter = THREE.NearestFilter
|
||||
capeTexture.minFilter = THREE.NearestFilter
|
||||
capeTexture.needsUpdate = true
|
||||
//@ts-ignore
|
||||
playerObject.cape.map = capeTexture
|
||||
//@ts-ignore
|
||||
playerObject.elytra.map = capeTexture
|
||||
playerObject.skin.rotation.y = Math.PI
|
||||
})
|
||||
} else {
|
||||
playerObject.backEquipment = null
|
||||
playerObject.elytra.map = null
|
||||
if (playerObject.cape.map) {
|
||||
playerObject.cape.map.dispose()
|
||||
}
|
||||
playerObject.cape.map = null
|
||||
}
|
||||
}
|
||||
|
||||
update (/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
|
||||
if (!this.entities[entity.id]) {
|
||||
const mesh = getEntityMesh(entity, this.scene, this.entitiesOptions)
|
||||
const group = new THREE.Group()
|
||||
let mesh
|
||||
if (entity.name === 'player') {
|
||||
const wrapper = new THREE.Group()
|
||||
const playerObject = new PlayerObject()
|
||||
playerObject.position.set(0, 16, 0)
|
||||
|
||||
//@ts-ignore
|
||||
wrapper.add(playerObject)
|
||||
const scale = 1 / 16
|
||||
wrapper.scale.set(scale, scale, scale)
|
||||
|
||||
//@ts-ignore
|
||||
group.playerObject = playerObject
|
||||
wrapper.rotation.set(0, Math.PI, 0)
|
||||
mesh = wrapper
|
||||
} else {
|
||||
mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides)
|
||||
}
|
||||
if (!mesh) return
|
||||
this.entities[entity.id] = mesh
|
||||
this.scene.add(mesh)
|
||||
// set initial position so there are no weird jumps update after
|
||||
group.position.set(entity.pos.x, entity.pos.y, entity.pos.z)
|
||||
|
||||
const boxHelper = new THREE.BoxHelper(mesh,
|
||||
entity.type === 'hostile' ? 0xff0000 :
|
||||
entity.type === 'mob' ? 0x00ff00 :
|
||||
entity.type === "player" ? 0x0000ff :
|
||||
0xffa500
|
||||
)
|
||||
group.add(mesh)
|
||||
group.add(boxHelper)
|
||||
this.scene.add(group)
|
||||
|
||||
this.entities[entity.id] = group
|
||||
|
||||
this.emit('add', entity)
|
||||
|
||||
if (entity.name === 'player') {
|
||||
this.updatePlayerSkin(entity.id, stevePng)
|
||||
}
|
||||
}
|
||||
|
||||
const e = this.entities[entity.id]
|
||||
|
||||
if (e.playerObject && overrides?.rotation?.head) {
|
||||
/** @type {PlayerObject} */
|
||||
const playerObject = e.playerObject
|
||||
const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0
|
||||
playerObject.skin.head.rotation.y = -headRotationDiff
|
||||
playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0
|
||||
}
|
||||
|
||||
if (entity.delete) {
|
||||
this.emit('remove', entity)
|
||||
this.scene.remove(e)
|
||||
dispose3(e)
|
||||
// todo dispose textures as well ?
|
||||
delete this.entities[entity.id]
|
||||
}
|
||||
|
||||
if (entity.pos) {
|
||||
new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, 50).start()
|
||||
new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, TWEEN_DURATION).start()
|
||||
}
|
||||
if (entity.yaw) {
|
||||
const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
|
||||
const dy = 2 * da % (Math.PI * 2) - da
|
||||
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, 50).start()
|
||||
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, TWEEN_DURATION).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Entities }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//@ts-check
|
||||
/* global THREE */
|
||||
|
||||
const entities = require('./entities.json')
|
||||
|
|
@ -128,7 +129,7 @@ function addCube (attr, boneId, bone, cube, texWidth = 64, texHeight = 64) {
|
|||
}
|
||||
}
|
||||
|
||||
function getMesh (texture, jsonModel) {
|
||||
function getMesh (texture, jsonModel, overrides = {}) {
|
||||
const bones = {}
|
||||
|
||||
const geoData = {
|
||||
|
|
@ -156,6 +157,12 @@ function getMesh (texture, jsonModel) {
|
|||
bone.rotation.y = -jsonBone.rotation[1] * Math.PI / 180
|
||||
bone.rotation.z = -jsonBone.rotation[2] * Math.PI / 180
|
||||
}
|
||||
if (overrides.rotation?.[jsonBone.name]) {
|
||||
bone.rotation.x -= (overrides.rotation[jsonBone.name].x ?? 0) * Math.PI / 180
|
||||
bone.rotation.y -= (overrides.rotation[jsonBone.name].y ?? 0) * Math.PI / 180
|
||||
bone.rotation.z -= (overrides.rotation[jsonBone.name].z ?? 0) * Math.PI / 180
|
||||
}
|
||||
bone.name = `bone_${jsonBone.name}`
|
||||
bones[jsonBone.name] = bone
|
||||
|
||||
if (jsonBone.cubes) {
|
||||
|
|
@ -169,7 +176,9 @@ function getMesh (texture, jsonModel) {
|
|||
const rootBones = []
|
||||
for (const jsonBone of jsonModel.bones) {
|
||||
if (jsonBone.parent) bones[jsonBone.parent].add(bones[jsonBone.name])
|
||||
else rootBones.push(bones[jsonBone.name])
|
||||
else {
|
||||
rootBones.push(bones[jsonBone.name])
|
||||
}
|
||||
}
|
||||
|
||||
const skeleton = new THREE.Skeleton(Object.values(bones))
|
||||
|
|
@ -189,6 +198,10 @@ function getMesh (texture, jsonModel) {
|
|||
mesh.scale.set(1 / 16, 1 / 16, 1 / 16)
|
||||
|
||||
loadTexture(texture, texture => {
|
||||
if (material.map) {
|
||||
// texture is already loaded
|
||||
return
|
||||
}
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
|
|
@ -208,14 +221,17 @@ const entitiesMap = {
|
|||
|
||||
// const unknownEntitiesSet = new Set()
|
||||
|
||||
class Entity {
|
||||
constructor (version, type, scene) {
|
||||
let mappedValue = entitiesMap[type]
|
||||
// todo is it okay?
|
||||
if (mappedValue === null) return
|
||||
else if (mappedValue) type = mappedValue
|
||||
const getEntity = (name) => {
|
||||
let mappedValue = entitiesMap[name]
|
||||
if (mappedValue) name = mappedValue
|
||||
|
||||
return entities[name]
|
||||
}
|
||||
|
||||
class Entity {
|
||||
constructor (version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
|
||||
const e = getEntity(type)
|
||||
|
||||
const e = entities[type]
|
||||
if (!e) {
|
||||
// if (unknownEntitiesSet.has(type)) throw new Error('ignore...')
|
||||
// unknownEntitiesSet.add(type)
|
||||
|
|
@ -224,16 +240,27 @@ class Entity {
|
|||
|
||||
this.mesh = new THREE.Object3D()
|
||||
for (const [name, jsonModel] of Object.entries(e.geometry)) {
|
||||
const texture = e.textures[name]
|
||||
const texture = overrides.textures?.[name] ?? e.textures[name]
|
||||
if (!texture) continue
|
||||
// console.log(JSON.stringify(jsonModel, null, 2))
|
||||
const mesh = getMesh(texture.replace('textures', 'textures/' + version) + '.png', jsonModel)
|
||||
/* const skeletonHelper = new THREE.SkeletonHelper( mesh )
|
||||
const mesh = getMesh(texture.replace('textures', 'textures/' + version) + '.png', jsonModel, overrides)
|
||||
mesh.name = `geometry_${name}`
|
||||
const skeletonHelper = new THREE.SkeletonHelper(mesh)
|
||||
//@ts-ignore
|
||||
skeletonHelper.material.linewidth = 2
|
||||
scene.add( skeletonHelper ) */
|
||||
this.mesh.add(skeletonHelper)
|
||||
skeletonHelper.visible = false
|
||||
this.mesh.add(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
static getStaticData (name) {
|
||||
const e = getEntity(name)
|
||||
if (!e) throw new Error(`Unknown entity ${name}`)
|
||||
return {
|
||||
boneNames: Object.values(e.geometry).flatMap(x => x.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Entity
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ export const loadScript = async function (/** @type {string} */scriptSrc) {
|
|||
})
|
||||
|
||||
scriptElement.onerror = (error) => {
|
||||
reject(error)
|
||||
reject(new Error(error.message))
|
||||
scriptElement.remove()
|
||||
}
|
||||
|
||||
document.head.appendChild(scriptElement)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { WorldRenderer } from './worldrenderer'
|
|||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { getVersion } from './version'
|
||||
import EventEmitter from 'events'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
|
|
@ -77,7 +78,15 @@ export class Viewer {
|
|||
}
|
||||
|
||||
updateEntity (e) {
|
||||
this.entities.update(e)
|
||||
this.entities.update(e, this.processEntityOverrides(e, {
|
||||
rotation: {
|
||||
head: {
|
||||
x: e.headPitch ?? e.pitch,
|
||||
y: e.headYaw,
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
updatePrimitive (p) {
|
||||
|
|
@ -122,7 +131,7 @@ export class Viewer {
|
|||
}
|
||||
|
||||
// todo type
|
||||
listen (emitter) {
|
||||
listen (emitter: EventEmitter) {
|
||||
emitter.on('entity', (e) => {
|
||||
this.updateEntity(e)
|
||||
})
|
||||
|
|
@ -154,7 +163,7 @@ export class Viewer {
|
|||
|
||||
emitter.emit('listening')
|
||||
|
||||
this.domElement.addEventListener('pointerdown', (evt) => {
|
||||
this.domElement.addEventListener?.('pointerdown', (evt) => {
|
||||
const raycaster = new THREE.Raycaster()
|
||||
const mouse = new THREE.Vector2()
|
||||
mouse.x = (evt.clientX / this.domElement.clientWidth) * 2 - 1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// global variables useful for debugging
|
||||
|
||||
import { getEntityCursor } from './worldInteractions'
|
||||
|
||||
// Object.defineProperty(window, 'cursorBlock', )
|
||||
|
||||
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
||||
|
|
@ -7,3 +9,7 @@ window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
|||
if (!newPos) return
|
||||
return bot.world.getBlock(newPos)
|
||||
}
|
||||
|
||||
window.cursorEntity = () => {
|
||||
return getEntityCursor()
|
||||
}
|
||||
|
|
|
|||
55
src/entities.ts
Normal file
55
src/entities.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Entity } from 'prismarine-entity'
|
||||
import { TextureLoader } from 'three'
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
const enableSkeletonHelpers = localStorage.enableSkeletonHelpers ?? false
|
||||
const entityData = (e: Entity) => {
|
||||
if (!e.username) return
|
||||
// const firstRender = !!window.debugEntityMetadata
|
||||
window.debugEntityMetadata ??= {}
|
||||
window.debugEntityMetadata[e.username] = e
|
||||
}
|
||||
|
||||
const entityFirstRendered = (e) => {
|
||||
const mesh = viewer.entities.entities[e.id]
|
||||
if (!mesh) throw new Error('mesh still not loaded')
|
||||
const visitChildren = (obj) => {
|
||||
if (!Array.isArray(obj?.children)) return
|
||||
const { children, isSkeletonHelper } = obj
|
||||
if (isSkeletonHelper && enableSkeletonHelpers) {
|
||||
obj.visible = true
|
||||
}
|
||||
if (e.type === 'player' && e.username) {
|
||||
if (e.name === 'geometry_default') {
|
||||
console.log('request', e.uuid)
|
||||
new TextureLoader().load(`https://mulv.tycrek.dev/api/lookup?username=${e.username}&type=skin`, (texture) => {
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
texture.wrapS = THREE.RepeatWrapping
|
||||
texture.wrapT = THREE.RepeatWrapping
|
||||
obj.material.map = texture
|
||||
})
|
||||
}
|
||||
if (e.name === 'geometry_cape') {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
for (const child of children) {
|
||||
if (typeof child === 'object') visitChildren(child)
|
||||
}
|
||||
}
|
||||
visitChildren(mesh)
|
||||
}
|
||||
|
||||
viewer.entities.addListener('add', entityFirstRendered)
|
||||
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
if (entity !== bot.entity) {
|
||||
entityData(entity)
|
||||
}
|
||||
}
|
||||
|
||||
bot.on('entitySpawn', entityData)
|
||||
bot.on('entityUpdate', entityData)
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//@ts-nocheck
|
||||
import EventEmitter from 'events'
|
||||
|
||||
window.bot = undefined
|
||||
window.THREE = undefined
|
||||
|
|
@ -6,3 +6,4 @@ window.localServer = undefined
|
|||
window.worldView = undefined
|
||||
window.viewer = undefined
|
||||
window.loadedData = undefined
|
||||
window.customEvents = new EventEmitter()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import './styles.css'
|
|||
import './globals'
|
||||
import 'iconify-icon'
|
||||
import './devtools'
|
||||
import './entities'
|
||||
import initCollisionShapes from './getCollisionShapes'
|
||||
import { onGameLoad } from './playerWindows'
|
||||
|
||||
|
|
@ -43,6 +44,8 @@ import worldInteractions from './worldInteractions'
|
|||
|
||||
import * as THREE from 'three'
|
||||
import MinecraftData, { versionsByMinecraftVersion } from 'minecraft-data'
|
||||
import debug from 'debug'
|
||||
import _ from 'lodash-es'
|
||||
|
||||
import { initVR } from './vr'
|
||||
import {
|
||||
|
|
@ -69,12 +72,9 @@ import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalS
|
|||
import defaultServerOptions from './defaultLocalServerOptions'
|
||||
import dayCycle from './dayCycle'
|
||||
|
||||
import _ from 'lodash-es'
|
||||
|
||||
import { genTexturePackTextures, watchTexturepackInViewer } from './texturePack'
|
||||
import { connectToPeer } from './localServerMultiplayer'
|
||||
import CustomChannelClient from './customClient'
|
||||
import debug from 'debug'
|
||||
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
|
||||
import { registerServiceWorker } from './serviceWorker'
|
||||
import { appStatusState, lastConnectOptions } from './react/AppStatusProvider'
|
||||
|
|
@ -85,11 +85,9 @@ import { loadInMemorySave } from './react/SingleplayerProvider'
|
|||
|
||||
// side effects
|
||||
import { downloadSoundsIfNeeded } from './soundSystem'
|
||||
import EventEmitter from 'events'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
window.customEvents = new EventEmitter()
|
||||
window.beforeRenderFrame = []
|
||||
|
||||
// ACTUAL CODE
|
||||
|
|
|
|||
|
|
@ -83,27 +83,7 @@ class WorldInteraction {
|
|||
if (!isGameActive(true)) return
|
||||
this.buttons[e.button] = true
|
||||
|
||||
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
|
||||
})
|
||||
const entity = getEntityCursor()
|
||||
|
||||
if (entity) {
|
||||
bot.attack(entity)
|
||||
|
|
@ -135,9 +115,10 @@ class WorldInteraction {
|
|||
|
||||
const upLineMaterial = () => {
|
||||
const inCreative = bot.game.gameMode === 'creative'
|
||||
const pixelRatio = viewer.renderer.getPixelRatio()
|
||||
this.lineMaterial = new LineMaterial({
|
||||
color: inCreative ? 0x40_80_ff : 0x00_00_00,
|
||||
linewidth: viewer.renderer.getPixelRatio() * 2,
|
||||
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
|
||||
// dashed: true,
|
||||
// dashSize: 5,
|
||||
})
|
||||
|
|
@ -319,4 +300,29 @@ const getDataFromShape = (shape) => {
|
|||
return { position, width, height, depth }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export default new WorldInteraction()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue