minecraft-renderer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +297 -0
- package/dist/index.html +83 -0
- package/dist/static/image/arrow.6f27b59f.png +0 -0
- package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
- package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
- package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
- package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
- package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
- package/dist/static/js/365.f05233ab.js +8462 -0
- package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
- package/dist/static/js/async/738.efa27644.js +1 -0
- package/dist/static/js/index.092ec5be.js +56 -0
- package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
- package/dist/static/js/lib-react.5c9129e0.js +2 -0
- package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
- package/package.json +104 -0
- package/src/assets/destroy_stage_0.png +0 -0
- package/src/assets/destroy_stage_1.png +0 -0
- package/src/assets/destroy_stage_2.png +0 -0
- package/src/assets/destroy_stage_3.png +0 -0
- package/src/assets/destroy_stage_4.png +0 -0
- package/src/assets/destroy_stage_5.png +0 -0
- package/src/assets/destroy_stage_6.png +0 -0
- package/src/assets/destroy_stage_7.png +0 -0
- package/src/assets/destroy_stage_8.png +0 -0
- package/src/assets/destroy_stage_9.png +0 -0
- package/src/examples/README.md +146 -0
- package/src/examples/appViewerExample.ts +205 -0
- package/src/examples/initialMenuStart.ts +161 -0
- package/src/graphicsBackend/appViewer.ts +297 -0
- package/src/graphicsBackend/config.ts +119 -0
- package/src/graphicsBackend/index.ts +10 -0
- package/src/graphicsBackend/playerState.ts +61 -0
- package/src/graphicsBackend/types.ts +143 -0
- package/src/index.ts +97 -0
- package/src/lib/DebugGui.ts +190 -0
- package/src/lib/animationController.ts +85 -0
- package/src/lib/buildSharedConfig.mjs +1 -0
- package/src/lib/cameraBobbing.ts +94 -0
- package/src/lib/canvas2DOverlay.example.ts +361 -0
- package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
- package/src/lib/canvas2DOverlay.ts +381 -0
- package/src/lib/cleanupDecorator.ts +29 -0
- package/src/lib/createPlayerObject.ts +55 -0
- package/src/lib/frameTimingCollector.ts +164 -0
- package/src/lib/guiRenderer.ts +283 -0
- package/src/lib/items.ts +140 -0
- package/src/lib/mesherlogReader.ts +131 -0
- package/src/lib/moreBlockDataGenerated.json +714 -0
- package/src/lib/preflatMap.json +1741 -0
- package/src/lib/simpleUtils.ts +40 -0
- package/src/lib/smoothSwitcher.ts +168 -0
- package/src/lib/spiral.ts +29 -0
- package/src/lib/ui/newStats.ts +120 -0
- package/src/lib/utils/proxy.ts +23 -0
- package/src/lib/utils/skins.ts +63 -0
- package/src/lib/utils.ts +76 -0
- package/src/lib/workerProxy.ts +342 -0
- package/src/lib/worldrendererCommon.ts +1088 -0
- package/src/mesher/mesher.ts +253 -0
- package/src/mesher/models.ts +769 -0
- package/src/mesher/modelsGeometryCommon.ts +142 -0
- package/src/mesher/shared.ts +80 -0
- package/src/mesher/standaloneRenderer.ts +270 -0
- package/src/mesher/test/a.ts +3 -0
- package/src/mesher/test/mesherTester.ts +76 -0
- package/src/mesher/test/playground.ts +19 -0
- package/src/mesher/test/test-perf.ts +74 -0
- package/src/mesher/test/tests.test.ts +56 -0
- package/src/mesher/world.ts +294 -0
- package/src/mesher/worldConstants.ts +1 -0
- package/src/modules/index.ts +11 -0
- package/src/modules/starfield.ts +313 -0
- package/src/modules/types.ts +110 -0
- package/src/playerState/playerState.ts +78 -0
- package/src/playerState/types.ts +36 -0
- package/src/playground/allEntitiesDebug.ts +170 -0
- package/src/playground/baseScene.ts +587 -0
- package/src/playground/mobileControls.tsx +268 -0
- package/src/playground/playground.html +83 -0
- package/src/playground/playground.ts +11 -0
- package/src/playground/playgroundUi.tsx +140 -0
- package/src/playground/reactUtils.ts +71 -0
- package/src/playground/scenes/allEntities.ts +13 -0
- package/src/playground/scenes/entities.ts +37 -0
- package/src/playground/scenes/floorRandom.ts +33 -0
- package/src/playground/scenes/frequentUpdates.ts +148 -0
- package/src/playground/scenes/geometryExport.ts +142 -0
- package/src/playground/scenes/index.ts +12 -0
- package/src/playground/scenes/lightingStarfield.ts +40 -0
- package/src/playground/scenes/main.ts +313 -0
- package/src/playground/scenes/railsCobweb.ts +14 -0
- package/src/playground/scenes/rotationIssue.ts +7 -0
- package/src/playground/scenes/slabsOptimization.ts +16 -0
- package/src/playground/scenes/transparencyIssue.ts +11 -0
- package/src/playground/shared.ts +79 -0
- package/src/resourcesManager/index.ts +5 -0
- package/src/resourcesManager/resourcesManager.ts +314 -0
- package/src/shims/minecraftData.ts +41 -0
- package/src/sign-renderer/index.html +21 -0
- package/src/sign-renderer/index.ts +216 -0
- package/src/sign-renderer/noop.js +1 -0
- package/src/sign-renderer/playground.ts +38 -0
- package/src/sign-renderer/tests.test.ts +69 -0
- package/src/sign-renderer/vite.config.ts +10 -0
- package/src/three/appShared.ts +75 -0
- package/src/three/bannerRenderer.ts +275 -0
- package/src/three/cameraShake.ts +120 -0
- package/src/three/cinimaticScript.ts +350 -0
- package/src/three/documentRenderer.ts +491 -0
- package/src/three/entities.ts +1580 -0
- package/src/three/entity/EntityMesh.ts +707 -0
- package/src/three/entity/animations.js +171 -0
- package/src/three/entity/armorModels.json +204 -0
- package/src/three/entity/armorModels.ts +36 -0
- package/src/three/entity/entities.json +6230 -0
- package/src/three/entity/exportedModels.js +38 -0
- package/src/three/entity/externalTextures.json +1 -0
- package/src/three/entity/models/allay.obj +325 -0
- package/src/three/entity/models/arrow.obj +60 -0
- package/src/three/entity/models/axolotl.obj +509 -0
- package/src/three/entity/models/blaze.obj +601 -0
- package/src/three/entity/models/boat.obj +417 -0
- package/src/three/entity/models/camel.obj +1061 -0
- package/src/three/entity/models/cat.obj +509 -0
- package/src/three/entity/models/chicken.obj +371 -0
- package/src/three/entity/models/cod.obj +371 -0
- package/src/three/entity/models/creeper.obj +279 -0
- package/src/three/entity/models/dolphin.obj +371 -0
- package/src/three/entity/models/ender_dragon.obj +2993 -0
- package/src/three/entity/models/enderman.obj +325 -0
- package/src/three/entity/models/endermite.obj +187 -0
- package/src/three/entity/models/fox.obj +463 -0
- package/src/three/entity/models/frog.obj +739 -0
- package/src/three/entity/models/ghast.obj +463 -0
- package/src/three/entity/models/goat.obj +601 -0
- package/src/three/entity/models/guardian.obj +1015 -0
- package/src/three/entity/models/horse.obj +1061 -0
- package/src/three/entity/models/llama.obj +509 -0
- package/src/three/entity/models/minecart.obj +233 -0
- package/src/three/entity/models/parrot.obj +509 -0
- package/src/three/entity/models/piglin.obj +739 -0
- package/src/three/entity/models/pillager.obj +371 -0
- package/src/three/entity/models/rabbit.obj +555 -0
- package/src/three/entity/models/sheep.obj +555 -0
- package/src/three/entity/models/shulker.obj +141 -0
- package/src/three/entity/models/sniffer.obj +693 -0
- package/src/three/entity/models/spider.obj +509 -0
- package/src/three/entity/models/tadpole.obj +95 -0
- package/src/three/entity/models/turtle.obj +371 -0
- package/src/three/entity/models/vex.obj +325 -0
- package/src/three/entity/models/villager.obj +509 -0
- package/src/three/entity/models/warden.obj +463 -0
- package/src/three/entity/models/witch.obj +647 -0
- package/src/three/entity/models/wolf.obj +509 -0
- package/src/three/entity/models/zombie_villager.obj +463 -0
- package/src/three/entity/objModels.js +1 -0
- package/src/three/fireworks.ts +661 -0
- package/src/three/fireworksRenderer.ts +434 -0
- package/src/three/globals.d.ts +7 -0
- package/src/three/graphicsBackend.ts +274 -0
- package/src/three/graphicsBackendOffThread.ts +107 -0
- package/src/three/hand.ts +89 -0
- package/src/three/holdingBlock.ts +926 -0
- package/src/three/index.ts +20 -0
- package/src/three/itemMesh.ts +427 -0
- package/src/three/modules.d.ts +14 -0
- package/src/three/panorama.ts +308 -0
- package/src/three/panoramaShared.ts +1 -0
- package/src/three/renderSlot.ts +82 -0
- package/src/three/skyboxRenderer.ts +406 -0
- package/src/three/starField.ts +13 -0
- package/src/three/threeJsMedia.ts +731 -0
- package/src/three/threeJsMethods.ts +15 -0
- package/src/three/threeJsParticles.ts +160 -0
- package/src/three/threeJsSound.ts +95 -0
- package/src/three/threeJsUtils.ts +90 -0
- package/src/three/waypointSprite.ts +435 -0
- package/src/three/waypoints.ts +163 -0
- package/src/three/world/cursorBlock.ts +172 -0
- package/src/three/world/vr.ts +257 -0
- package/src/three/worldGeometryExport.ts +259 -0
- package/src/three/worldGeometryHandler.ts +279 -0
- package/src/three/worldRendererThree.ts +1381 -0
- package/src/worldView/index.ts +6 -0
- package/src/worldView/types.ts +66 -0
- package/src/worldView/worldView.ts +424 -0
|
@@ -0,0 +1,1580 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
import { UnionToIntersection } from 'type-fest'
|
|
3
|
+
import nbt from 'prismarine-nbt'
|
|
4
|
+
import * as TWEEN from '@tweenjs/tween.js'
|
|
5
|
+
import * as THREE from 'three'
|
|
6
|
+
import { PlayerAnimation, PlayerObject } from 'skinview3d'
|
|
7
|
+
import { inferModelType, loadCapeToCanvas, loadEarsToCanvasFromSkin } from 'skinview-utils'
|
|
8
|
+
// todo replace with url
|
|
9
|
+
import { flat, fromFormattedString } from '@xmcl/text-component'
|
|
10
|
+
import mojangson from 'mojangson'
|
|
11
|
+
import { snakeCase } from 'change-case'
|
|
12
|
+
import { Item } from 'prismarine-item'
|
|
13
|
+
import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity'
|
|
14
|
+
import { Team } from 'mineflayer'
|
|
15
|
+
import PrismarineChatLoader from 'prismarine-chat'
|
|
16
|
+
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
|
|
17
|
+
import { renderComponent } from '../sign-renderer'
|
|
18
|
+
import { createCanvas } from '../lib/utils'
|
|
19
|
+
import { PlayerObjectType } from '../lib/createPlayerObject'
|
|
20
|
+
import { getBlockMeshFromModel } from './holdingBlock'
|
|
21
|
+
import { createItemMesh } from './itemMesh'
|
|
22
|
+
import * as Entity from './entity/EntityMesh'
|
|
23
|
+
import { getMesh } from './entity/EntityMesh'
|
|
24
|
+
import { WalkingGeneralSwing } from './entity/animations'
|
|
25
|
+
import { disposeObject, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
|
|
26
|
+
import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
|
|
27
|
+
import { WorldRendererThree } from './worldRendererThree'
|
|
28
|
+
import { IndexedData } from 'minecraft-data'
|
|
29
|
+
import { ItemSpecificContextProperties } from '@/playerState/types'
|
|
30
|
+
|
|
31
|
+
// Type for entity metadata - simplified version
|
|
32
|
+
type EntityMetadataVersions = {
|
|
33
|
+
[key: string]: any
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
|
|
37
|
+
|
|
38
|
+
export const TWEEN_DURATION = 120
|
|
39
|
+
|
|
40
|
+
const degreesToRadians = (degrees: number) => degrees * (Math.PI / 180)
|
|
41
|
+
|
|
42
|
+
function convert2sComplementToHex(complement: number) {
|
|
43
|
+
if (complement < 0) {
|
|
44
|
+
complement = (0xFF_FF_FF_FF + complement + 1) >>> 0
|
|
45
|
+
}
|
|
46
|
+
return complement.toString(16)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toRgba(color: string | undefined) {
|
|
50
|
+
if (color === undefined) {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
if (parseInt(color, 10) === 0) {
|
|
54
|
+
return 'rgba(0, 0, 0, 0)'
|
|
55
|
+
}
|
|
56
|
+
const hex = convert2sComplementToHex(parseInt(color, 10))
|
|
57
|
+
if (hex.length === 8) {
|
|
58
|
+
return `#${hex.slice(2, 8)}${hex.slice(0, 2)}`
|
|
59
|
+
} else {
|
|
60
|
+
return `#${hex}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toQuaternion(quaternion: any, defaultValue?: THREE.Quaternion) {
|
|
65
|
+
if (quaternion === undefined) {
|
|
66
|
+
return defaultValue
|
|
67
|
+
}
|
|
68
|
+
if (quaternion instanceof THREE.Quaternion) {
|
|
69
|
+
return quaternion
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(quaternion)) {
|
|
72
|
+
return new THREE.Quaternion(quaternion[0], quaternion[1], quaternion[2], quaternion[3])
|
|
73
|
+
}
|
|
74
|
+
return new THREE.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function poseToEuler(pose: any, defaultValue?: THREE.Euler) {
|
|
78
|
+
if (pose === undefined) {
|
|
79
|
+
return defaultValue ?? new THREE.Euler()
|
|
80
|
+
}
|
|
81
|
+
if (pose instanceof THREE.Euler) {
|
|
82
|
+
return pose
|
|
83
|
+
}
|
|
84
|
+
if (pose['yaw'] !== undefined && pose['pitch'] !== undefined && pose['roll'] !== undefined) {
|
|
85
|
+
// Convert Minecraft pitch, yaw, roll definitions to our angle system
|
|
86
|
+
return new THREE.Euler(-degreesToRadians(pose.pitch), -degreesToRadians(pose.yaw), degreesToRadians(pose.roll), 'ZYX')
|
|
87
|
+
}
|
|
88
|
+
if (pose['x'] !== undefined && pose['y'] !== undefined && pose['z'] !== undefined) {
|
|
89
|
+
return new THREE.Euler(pose.z, pose.y, pose.x, 'ZYX')
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(pose)) {
|
|
92
|
+
return new THREE.Euler(pose[0], pose[1], pose[2])
|
|
93
|
+
}
|
|
94
|
+
return defaultValue ?? new THREE.Euler()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getUsernameTexture({
|
|
98
|
+
username,
|
|
99
|
+
nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
|
|
100
|
+
nameTagTextOpacity = 255
|
|
101
|
+
}: any, { fontFamily = 'mojangles' }: any, version: string) {
|
|
102
|
+
const canvas = createCanvas(64, 64)
|
|
103
|
+
|
|
104
|
+
const PrismarineChat = PrismarineChatLoader(version)
|
|
105
|
+
|
|
106
|
+
const ctx = canvas.getContext('2d')
|
|
107
|
+
if (!ctx) throw new Error('Could not get 2d context')
|
|
108
|
+
|
|
109
|
+
const fontSize = 48
|
|
110
|
+
const padding = 5
|
|
111
|
+
ctx.font = `${fontSize}px ${fontFamily}`
|
|
112
|
+
|
|
113
|
+
const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n')
|
|
114
|
+
let textWidth = 0
|
|
115
|
+
for (const line of plainLines) {
|
|
116
|
+
const width = ctx.measureText(line).width + padding * 2
|
|
117
|
+
if (width > textWidth) textWidth = width
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
canvas.width = textWidth
|
|
121
|
+
canvas.height = (fontSize + padding) * plainLines.length
|
|
122
|
+
|
|
123
|
+
ctx.fillStyle = nameTagBackgroundColor
|
|
124
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
125
|
+
|
|
126
|
+
ctx.globalAlpha = nameTagTextOpacity / 255
|
|
127
|
+
|
|
128
|
+
renderComponent(username, PrismarineChat, canvas, fontSize, 'white', -padding + fontSize)
|
|
129
|
+
|
|
130
|
+
ctx.globalAlpha = 1
|
|
131
|
+
|
|
132
|
+
return canvas
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const addNametag = (entity, options: { fontFamily: string }, mesh, version: string) => {
|
|
136
|
+
for (const c of mesh.children) {
|
|
137
|
+
if (c.name === 'nametag') {
|
|
138
|
+
c.removeFromParent()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (entity.username !== undefined) {
|
|
142
|
+
const canvas = getUsernameTexture(entity, options, version)
|
|
143
|
+
const tex = new THREE.Texture(canvas)
|
|
144
|
+
tex.needsUpdate = true
|
|
145
|
+
let nameTag: THREE.Object3D
|
|
146
|
+
if (entity.nameTagFixed) {
|
|
147
|
+
const geometry = new THREE.PlaneGeometry()
|
|
148
|
+
const material = new THREE.MeshBasicMaterial({ map: tex })
|
|
149
|
+
material.transparent = true
|
|
150
|
+
nameTag = new THREE.Mesh(geometry, material)
|
|
151
|
+
nameTag.rotation.set(entity.pitch, THREE.MathUtils.degToRad(entity.yaw + 180), 0)
|
|
152
|
+
nameTag.position.y += entity.height + 0.3
|
|
153
|
+
} else {
|
|
154
|
+
const spriteMat = new THREE.SpriteMaterial({ map: tex })
|
|
155
|
+
nameTag = new THREE.Sprite(spriteMat)
|
|
156
|
+
nameTag.position.y += entity.height + 0.6
|
|
157
|
+
}
|
|
158
|
+
nameTag.renderOrder = 1000
|
|
159
|
+
nameTag.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
|
|
160
|
+
if (entity.nameTagRotationRight) {
|
|
161
|
+
nameTag.applyQuaternion(entity.nameTagRotationRight)
|
|
162
|
+
}
|
|
163
|
+
if (entity.nameTagScale) {
|
|
164
|
+
nameTag.scale.multiply(entity.nameTagScale)
|
|
165
|
+
}
|
|
166
|
+
if (entity.nameTagRotationLeft) {
|
|
167
|
+
nameTag.applyQuaternion(entity.nameTagRotationLeft)
|
|
168
|
+
}
|
|
169
|
+
if (entity.nameTagTranslation) {
|
|
170
|
+
nameTag.position.add(entity.nameTagTranslation)
|
|
171
|
+
}
|
|
172
|
+
nameTag.name = 'nametag'
|
|
173
|
+
|
|
174
|
+
mesh.add(nameTag)
|
|
175
|
+
return nameTag
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// todo cleanup
|
|
180
|
+
const nametags = {}
|
|
181
|
+
|
|
182
|
+
const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
|
|
183
|
+
|
|
184
|
+
function getEntityMesh(mcData: IndexedData | undefined, entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree, options: { fontFamily: string }, overrides) {
|
|
185
|
+
if (entity.name) {
|
|
186
|
+
try {
|
|
187
|
+
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
|
188
|
+
const entityName = (isFirstUpperCase(entity.name) ? snakeCase(entity.name) : entity.name).toLowerCase()
|
|
189
|
+
const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides)
|
|
190
|
+
|
|
191
|
+
if (e.mesh) {
|
|
192
|
+
addNametag(entity, options, e.mesh, world.version)
|
|
193
|
+
return e.mesh
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
reportError?.(err)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!mcData || !isEntityAttackable(mcData, entity)) return
|
|
201
|
+
const geometry = new THREE.BoxGeometry(entity.width, entity.height, entity.width)
|
|
202
|
+
geometry.translate(0, entity.height / 2, 0)
|
|
203
|
+
const material = new THREE.MeshBasicMaterial({ color: 0xff_00_ff })
|
|
204
|
+
const cube = new THREE.Mesh(geometry, material)
|
|
205
|
+
const nametagCount = (nametags[entity.name] = (nametags[entity.name] || 0) + 1)
|
|
206
|
+
if (nametagCount < 6) {
|
|
207
|
+
addNametag({
|
|
208
|
+
username: entity.name,
|
|
209
|
+
height: entity.height,
|
|
210
|
+
}, options, cube, world.version)
|
|
211
|
+
}
|
|
212
|
+
return cube
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type SceneEntity = THREE.Object3D & {
|
|
216
|
+
playerObject?: PlayerObjectType
|
|
217
|
+
username?: string
|
|
218
|
+
uuid?: string
|
|
219
|
+
additionalCleanup?: () => void
|
|
220
|
+
originalEntity: import('prismarine-entity').Entity & { delete?; pos?, name, team?: Team }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class Entities {
|
|
224
|
+
entities = {} as Record<string, SceneEntity>
|
|
225
|
+
playerEntity: SceneEntity | null = null // Special entity for the player in third person
|
|
226
|
+
entitiesOptions = {
|
|
227
|
+
fontFamily: 'mojangles'
|
|
228
|
+
}
|
|
229
|
+
debugMode: string
|
|
230
|
+
onSkinUpdate: () => void
|
|
231
|
+
clock = new THREE.Clock()
|
|
232
|
+
currentlyRendering = true
|
|
233
|
+
cachedMapsImages = {} as Record<number, string>
|
|
234
|
+
itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
|
|
235
|
+
pendingModelOverrides = new Map<string, { modelPath: string, modelType: Entity.EntityModelType, metadata: any }>()
|
|
236
|
+
|
|
237
|
+
get entitiesByName(): Record<string, SceneEntity[]> {
|
|
238
|
+
const byName: Record<string, SceneEntity[]> = {}
|
|
239
|
+
for (const entity of Object.values(this.entities)) {
|
|
240
|
+
if (!entity['realName']) continue
|
|
241
|
+
byName[entity['realName']] = byName[entity['realName']] || []
|
|
242
|
+
byName[entity['realName']].push(entity)
|
|
243
|
+
}
|
|
244
|
+
return byName
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
get entitiesRenderingCount(): number {
|
|
248
|
+
return Object.values(this.entities).filter(entity => entity.visible).length
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getDebugString(): string {
|
|
252
|
+
const totalEntities = Object.keys(this.entities).length
|
|
253
|
+
const visibleEntities = this.entitiesRenderingCount
|
|
254
|
+
|
|
255
|
+
const playerEntities = Object.values(this.entities).filter(entity => entity.playerObject)
|
|
256
|
+
const visiblePlayerEntities = playerEntities.filter(entity => entity.visible)
|
|
257
|
+
|
|
258
|
+
return `${visibleEntities}/${totalEntities} ${visiblePlayerEntities.length}/${playerEntities.length}`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
constructor(public worldRenderer: WorldRendererThree, public mcData?: IndexedData) {
|
|
262
|
+
this.debugMode = 'none'
|
|
263
|
+
this.onSkinUpdate = () => { }
|
|
264
|
+
this.watchResourcesUpdates()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
handlePlayerEntity(playerData: SceneEntity['originalEntity']) {
|
|
268
|
+
// Create player entity if it doesn't exist
|
|
269
|
+
if (!this.playerEntity) {
|
|
270
|
+
// Create the player entity similar to how normal entities are created
|
|
271
|
+
const group = new THREE.Group() as unknown as SceneEntity
|
|
272
|
+
group.originalEntity = { ...playerData, name: 'player' } as SceneEntity['originalEntity']
|
|
273
|
+
|
|
274
|
+
const wrapper = new THREE.Group()
|
|
275
|
+
const playerObject = this.setupPlayerObject(playerData, wrapper, {})
|
|
276
|
+
group.playerObject = playerObject
|
|
277
|
+
group.add(wrapper)
|
|
278
|
+
|
|
279
|
+
group.name = 'player_entity'
|
|
280
|
+
this.playerEntity = group
|
|
281
|
+
this.worldRenderer.scene.add(group)
|
|
282
|
+
|
|
283
|
+
void this.updatePlayerSkin(playerData.id, playerData.username, playerData.uuid ?? undefined, stevePngUrl)
|
|
284
|
+
}
|
|
285
|
+
this.playerEntity.originalEntity = { ...playerData, name: 'player' } as SceneEntity['originalEntity']
|
|
286
|
+
|
|
287
|
+
// Update position and rotation
|
|
288
|
+
if (playerData.position) {
|
|
289
|
+
this.playerEntity.position.set(playerData.position.x, playerData.position.y, playerData.position.z)
|
|
290
|
+
}
|
|
291
|
+
if (playerData.yaw !== undefined) {
|
|
292
|
+
this.playerEntity.rotation.y = playerData.yaw
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.updateEntityEquipment(this.playerEntity, playerData)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
clear() {
|
|
299
|
+
for (const mesh of Object.values(this.entities)) {
|
|
300
|
+
this.worldRenderer.scene.remove(mesh)
|
|
301
|
+
disposeObject(mesh)
|
|
302
|
+
}
|
|
303
|
+
this.entities = {}
|
|
304
|
+
this.currentSkinUrls = {}
|
|
305
|
+
|
|
306
|
+
// Clean up player entity
|
|
307
|
+
if (this.playerEntity) {
|
|
308
|
+
this.worldRenderer.scene.remove(this.playerEntity)
|
|
309
|
+
disposeObject(this.playerEntity)
|
|
310
|
+
this.playerEntity = null
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
reloadEntities() {
|
|
315
|
+
for (const entity of Object.values(this.entities)) {
|
|
316
|
+
// update all entities textures like held items, armour, etc
|
|
317
|
+
// todo update entity textures itself
|
|
318
|
+
this.update({ ...entity.originalEntity, delete: true, } as SceneEntity['originalEntity'], {})
|
|
319
|
+
this.update(entity.originalEntity, {})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
watchResourcesUpdates() {
|
|
324
|
+
this.worldRenderer.resourcesManager.on('assetsTexturesUpdated', () => this.reloadEntities())
|
|
325
|
+
this.worldRenderer.resourcesManager.on('assetsInventoryReady', () => this.reloadEntities())
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setDebugMode(mode: string, entity: THREE.Object3D | null = null) {
|
|
329
|
+
this.debugMode = mode
|
|
330
|
+
for (const mesh of entity ? [entity] : Object.values(this.entities)) {
|
|
331
|
+
const boxHelper = mesh.children.find(c => c.name === 'debug')!
|
|
332
|
+
boxHelper.visible = false
|
|
333
|
+
if (this.debugMode === 'basic') {
|
|
334
|
+
boxHelper.visible = true
|
|
335
|
+
}
|
|
336
|
+
// todo advanced
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setRendering(rendering: boolean, entity: THREE.Object3D | null = null) {
|
|
341
|
+
this.currentlyRendering = rendering
|
|
342
|
+
for (const ent of entity ? [entity] : Object.values(this.entities)) {
|
|
343
|
+
if (rendering) {
|
|
344
|
+
if (!this.worldRenderer.scene.children.includes(ent)) this.worldRenderer.scene.add(ent)
|
|
345
|
+
} else {
|
|
346
|
+
this.worldRenderer.scene.remove(ent)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
playEntityModelAnimation(entityId: string, animationName: string, loop = false) {
|
|
352
|
+
const entity = this.entities[entityId]
|
|
353
|
+
if (!entity) return
|
|
354
|
+
|
|
355
|
+
entity.traverse(child => {
|
|
356
|
+
if (child instanceof Entity.EntityMesh) {
|
|
357
|
+
child.playAnimation(animationName, loop)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
render() {
|
|
363
|
+
const renderEntitiesConfig = this.worldRenderer.worldRendererConfig.renderEntities
|
|
364
|
+
if (renderEntitiesConfig !== this.currentlyRendering) {
|
|
365
|
+
this.setRendering(renderEntitiesConfig)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const dt = this.clock.getDelta()
|
|
369
|
+
const botPos = this.worldRenderer.viewerChunkPosition
|
|
370
|
+
const VISIBLE_DISTANCE = 10 * 10
|
|
371
|
+
|
|
372
|
+
// Update regular entities
|
|
373
|
+
for (const [entityId, entity] of [...Object.entries(this.entities), ['player_entity', this.playerEntity] as [string, SceneEntity | null]]) {
|
|
374
|
+
if (!entity) continue
|
|
375
|
+
const { playerObject } = entity
|
|
376
|
+
|
|
377
|
+
// Update animations
|
|
378
|
+
if (playerObject?.animation) {
|
|
379
|
+
playerObject.animation.update(playerObject, dt)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Update GLTF animations
|
|
383
|
+
entity.traverse(child => {
|
|
384
|
+
if (child instanceof Entity.EntityMesh) {
|
|
385
|
+
child.update(dt)
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// Update visibility based on distance and chunk load status
|
|
390
|
+
if (botPos && entity.position) {
|
|
391
|
+
const dx = entity.position.x - botPos.x
|
|
392
|
+
const dy = entity.position.y - botPos.y
|
|
393
|
+
const dz = entity.position.z - botPos.z
|
|
394
|
+
const distanceSquared = dx * dx + dy * dy + dz * dz
|
|
395
|
+
|
|
396
|
+
// Entity is visible if within 20 blocks OR in a finished chunk
|
|
397
|
+
entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.worldRenderer.shouldObjectVisible(entity))
|
|
398
|
+
|
|
399
|
+
this.maybeRenderPlayerSkin(entityId)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (entity.visible) {
|
|
403
|
+
// Update armor positions
|
|
404
|
+
this.syncArmorPositions(entity)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (entityId === 'player_entity') {
|
|
408
|
+
entity.visible = this.worldRenderer.playerStateUtils.isThirdPerson()
|
|
409
|
+
|
|
410
|
+
if (entity.visible) {
|
|
411
|
+
// sync
|
|
412
|
+
const yOffset = this.worldRenderer.playerStateReactive.eyeHeight
|
|
413
|
+
const pos = this.worldRenderer.cameraObject.position.clone().add(new THREE.Vector3(0, -yOffset, 0))
|
|
414
|
+
entity.position.set(pos.x, pos.y, pos.z)
|
|
415
|
+
|
|
416
|
+
const rotation = this.worldRenderer.cameraShake.getBaseRotation()
|
|
417
|
+
entity.rotation.set(0, rotation.yaw, 0)
|
|
418
|
+
|
|
419
|
+
// Sync head rotation
|
|
420
|
+
entity.traverse((c) => {
|
|
421
|
+
if (c.name === 'head') {
|
|
422
|
+
c.rotation.set(-rotation.pitch, 0, 0)
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private syncArmorPositions(entity: SceneEntity) {
|
|
431
|
+
if (!entity.playerObject) return
|
|
432
|
+
|
|
433
|
+
// todo-low use property access for less loop iterations (small performance gain)
|
|
434
|
+
entity.traverse((armor) => {
|
|
435
|
+
if (!armor.name.startsWith('geometry_armor_')) return
|
|
436
|
+
|
|
437
|
+
const { skin } = entity.playerObject!
|
|
438
|
+
|
|
439
|
+
switch (armor.name) {
|
|
440
|
+
case 'geometry_armor_head':
|
|
441
|
+
// Head armor sync
|
|
442
|
+
if (armor.children[0]?.children[0]) {
|
|
443
|
+
armor.children[0].children[0].rotation.set(
|
|
444
|
+
-skin.head.rotation.x,
|
|
445
|
+
skin.head.rotation.y,
|
|
446
|
+
skin.head.rotation.z,
|
|
447
|
+
skin.head.rotation.order
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
break
|
|
451
|
+
|
|
452
|
+
case 'geometry_armor_legs':
|
|
453
|
+
// Legs armor sync
|
|
454
|
+
if (armor.children[0]) {
|
|
455
|
+
// Left leg
|
|
456
|
+
if (armor.children[0].children[2]) {
|
|
457
|
+
armor.children[0].children[2].rotation.set(
|
|
458
|
+
-skin.leftLeg.rotation.x,
|
|
459
|
+
skin.leftLeg.rotation.y,
|
|
460
|
+
skin.leftLeg.rotation.z,
|
|
461
|
+
skin.leftLeg.rotation.order
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
// Right leg
|
|
465
|
+
if (armor.children[0].children[1]) {
|
|
466
|
+
armor.children[0].children[1].rotation.set(
|
|
467
|
+
-skin.rightLeg.rotation.x,
|
|
468
|
+
skin.rightLeg.rotation.y,
|
|
469
|
+
skin.rightLeg.rotation.z,
|
|
470
|
+
skin.rightLeg.rotation.order
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
break
|
|
475
|
+
|
|
476
|
+
case 'geometry_armor_feet':
|
|
477
|
+
// Boots armor sync
|
|
478
|
+
if (armor.children[0]) {
|
|
479
|
+
// Right boot
|
|
480
|
+
if (armor.children[0].children[0]) {
|
|
481
|
+
armor.children[0].children[0].rotation.set(
|
|
482
|
+
-skin.rightLeg.rotation.x,
|
|
483
|
+
skin.rightLeg.rotation.y,
|
|
484
|
+
skin.rightLeg.rotation.z,
|
|
485
|
+
skin.rightLeg.rotation.order
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
// Left boot (reversed Z rotation)
|
|
489
|
+
if (armor.children[0].children[1]) {
|
|
490
|
+
armor.children[0].children[1].rotation.set(
|
|
491
|
+
-skin.leftLeg.rotation.x,
|
|
492
|
+
skin.leftLeg.rotation.y,
|
|
493
|
+
-skin.leftLeg.rotation.z,
|
|
494
|
+
skin.leftLeg.rotation.order
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
break
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
getPlayerObject(entityId: string | number) {
|
|
504
|
+
if (this.playerEntity?.originalEntity.id === entityId) return this.playerEntity?.playerObject
|
|
505
|
+
const playerObject = this.entities[entityId]?.playerObject
|
|
506
|
+
return playerObject
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
|
|
510
|
+
currentSkinUrls = {} as Record<string, string>
|
|
511
|
+
|
|
512
|
+
private isCanvasBlank(canvas: HTMLCanvasElement): boolean {
|
|
513
|
+
return !canvas.getContext('2d')
|
|
514
|
+
?.getImageData(0, 0, canvas.width, canvas.height).data
|
|
515
|
+
.some(channel => channel !== 0)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// todo true/undefined doesnt reset the skin to the default one
|
|
519
|
+
// eslint-disable-next-line max-params
|
|
520
|
+
async updatePlayerSkin(entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
|
521
|
+
const isCustomSkin = skinUrl !== stevePngUrl
|
|
522
|
+
if (isCustomSkin) {
|
|
523
|
+
this.loadedSkinEntityIds.add(String(entityId))
|
|
524
|
+
}
|
|
525
|
+
if (uuidCache) {
|
|
526
|
+
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache] = {}
|
|
527
|
+
if (typeof skinUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].skinUrl = skinUrl
|
|
528
|
+
if (typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuidCache].capeUrl = capeUrl
|
|
529
|
+
if (skinUrl === true) {
|
|
530
|
+
skinUrl = this.uuidPerSkinUrlsCache[uuidCache]?.skinUrl ?? skinUrl
|
|
531
|
+
}
|
|
532
|
+
capeUrl ??= this.uuidPerSkinUrlsCache[uuidCache]?.capeUrl
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const playerObject = this.getPlayerObject(entityId)
|
|
536
|
+
if (!playerObject) return
|
|
537
|
+
|
|
538
|
+
if (skinUrl === true) {
|
|
539
|
+
if (!username) return
|
|
540
|
+
const newSkinUrl = await loadSkinFromUsername(username, 'skin')
|
|
541
|
+
if (!this.getPlayerObject(entityId)) return
|
|
542
|
+
if (!newSkinUrl) return
|
|
543
|
+
skinUrl = newSkinUrl
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (typeof skinUrl !== 'string') throw new Error('Invalid skin url')
|
|
547
|
+
|
|
548
|
+
// Skip if same skin URL is already loaded for this entity
|
|
549
|
+
if (this.currentSkinUrls[String(entityId)] === skinUrl) {
|
|
550
|
+
// Still handle cape if needed
|
|
551
|
+
if (capeUrl) {
|
|
552
|
+
if (capeUrl === true && username) {
|
|
553
|
+
const newCapeUrl = await loadSkinFromUsername(username, 'cape')
|
|
554
|
+
if (!this.getPlayerObject(entityId)) return
|
|
555
|
+
if (!newCapeUrl) return
|
|
556
|
+
capeUrl = newCapeUrl
|
|
557
|
+
}
|
|
558
|
+
if (typeof capeUrl === 'string') {
|
|
559
|
+
void this.loadAndApplyCape(entityId, capeUrl)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (skinUrl !== stevePngUrl) {
|
|
566
|
+
this.currentSkinUrls[String(entityId)] = skinUrl
|
|
567
|
+
}
|
|
568
|
+
const renderEars = this.worldRenderer.worldRendererConfig.renderEars || username === 'deadmau5'
|
|
569
|
+
void this.loadAndApplySkin(entityId, skinUrl, renderEars).then(async () => {
|
|
570
|
+
if (capeUrl) {
|
|
571
|
+
if (capeUrl === true && username) {
|
|
572
|
+
const newCapeUrl = await loadSkinFromUsername(username, 'cape')
|
|
573
|
+
if (!this.getPlayerObject(entityId)) return
|
|
574
|
+
if (!newCapeUrl) return
|
|
575
|
+
capeUrl = newCapeUrl
|
|
576
|
+
}
|
|
577
|
+
if (typeof capeUrl === 'string') {
|
|
578
|
+
void this.loadAndApplyCape(entityId, capeUrl)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
playerObject.cape.visible = false
|
|
585
|
+
if (!capeUrl) {
|
|
586
|
+
playerObject.backEquipment = null
|
|
587
|
+
playerObject.elytra.map = null
|
|
588
|
+
if (playerObject.cape.map) {
|
|
589
|
+
playerObject.cape.map.dispose()
|
|
590
|
+
}
|
|
591
|
+
playerObject.cape.map = null
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private async loadAndApplySkin(entityId: string | number, skinUrl: string, renderEars: boolean) {
|
|
596
|
+
let playerObject = this.getPlayerObject(entityId)
|
|
597
|
+
if (!playerObject) return
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
let playerCustomSkinImage: ImageBitmap | undefined
|
|
601
|
+
|
|
602
|
+
playerObject = this.getPlayerObject(entityId)
|
|
603
|
+
if (!playerObject) return
|
|
604
|
+
|
|
605
|
+
let skinTexture: THREE.Texture
|
|
606
|
+
let skinCanvas: OffscreenCanvas
|
|
607
|
+
if (skinUrl === stevePngUrl) {
|
|
608
|
+
skinTexture = await steveTexture
|
|
609
|
+
const canvas = createCanvas(64, 64)
|
|
610
|
+
const ctx = canvas.getContext('2d')
|
|
611
|
+
if (!ctx) throw new Error('Failed to get context')
|
|
612
|
+
ctx.drawImage(skinTexture.image, 0, 0)
|
|
613
|
+
skinCanvas = canvas
|
|
614
|
+
} else {
|
|
615
|
+
const { canvas, image } = await loadSkinImage(skinUrl)
|
|
616
|
+
playerCustomSkinImage = image
|
|
617
|
+
skinTexture = new THREE.CanvasTexture(canvas)
|
|
618
|
+
skinCanvas = canvas
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
skinTexture.magFilter = THREE.NearestFilter
|
|
622
|
+
skinTexture.minFilter = THREE.NearestFilter
|
|
623
|
+
skinTexture.needsUpdate = true
|
|
624
|
+
playerObject.skin.map = skinTexture as any
|
|
625
|
+
playerObject.skin.modelType = inferModelType(skinCanvas)
|
|
626
|
+
|
|
627
|
+
let earsCanvas: HTMLCanvasElement | undefined
|
|
628
|
+
if (!playerCustomSkinImage) {
|
|
629
|
+
renderEars = false
|
|
630
|
+
} else if (renderEars) {
|
|
631
|
+
earsCanvas = document.createElement('canvas')
|
|
632
|
+
loadEarsToCanvasFromSkin(earsCanvas, playerCustomSkinImage)
|
|
633
|
+
renderEars = !this.isCanvasBlank(earsCanvas)
|
|
634
|
+
}
|
|
635
|
+
if (renderEars) {
|
|
636
|
+
const earsTexture = new THREE.CanvasTexture(earsCanvas!)
|
|
637
|
+
earsTexture.magFilter = THREE.NearestFilter
|
|
638
|
+
earsTexture.minFilter = THREE.NearestFilter
|
|
639
|
+
earsTexture.needsUpdate = true
|
|
640
|
+
//@ts-expect-error
|
|
641
|
+
playerObject.ears.map = earsTexture
|
|
642
|
+
playerObject.ears.visible = true
|
|
643
|
+
} else {
|
|
644
|
+
playerObject.ears.map = null
|
|
645
|
+
playerObject.ears.visible = false
|
|
646
|
+
}
|
|
647
|
+
this.onSkinUpdate?.()
|
|
648
|
+
} catch (error) {
|
|
649
|
+
console.error('Error loading skin:', error)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private async loadAndApplyCape(entityId: string | number, capeUrl: string) {
|
|
654
|
+
let playerObject = this.getPlayerObject(entityId)
|
|
655
|
+
if (!playerObject) return
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const { canvas: capeCanvas, image: capeImage } = await loadSkinImage(capeUrl)
|
|
659
|
+
|
|
660
|
+
playerObject = this.getPlayerObject(entityId)
|
|
661
|
+
if (!playerObject) return
|
|
662
|
+
|
|
663
|
+
loadCapeToCanvas(capeCanvas, capeImage)
|
|
664
|
+
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
|
665
|
+
capeTexture.magFilter = THREE.NearestFilter
|
|
666
|
+
capeTexture.minFilter = THREE.NearestFilter
|
|
667
|
+
capeTexture.needsUpdate = true
|
|
668
|
+
//@ts-expect-error
|
|
669
|
+
playerObject.cape.map = capeTexture
|
|
670
|
+
playerObject.cape.visible = true
|
|
671
|
+
//@ts-expect-error
|
|
672
|
+
playerObject.elytra.map = capeTexture
|
|
673
|
+
this.onSkinUpdate?.()
|
|
674
|
+
|
|
675
|
+
if (!playerObject.backEquipment) {
|
|
676
|
+
playerObject.backEquipment = 'cape'
|
|
677
|
+
}
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.error('Error loading cape:', error)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
debugSwingArm() {
|
|
684
|
+
const playerObject = Object.values(this.entities).find(entity => entity.playerObject?.animation)
|
|
685
|
+
if (!playerObject || !playerObject.playerObject?.animation) return
|
|
686
|
+
const anim = playerObject.playerObject.animation as any
|
|
687
|
+
if (anim.swingArm) {
|
|
688
|
+
anim.swingArm()
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
|
|
693
|
+
// TODO CLEANUP!
|
|
694
|
+
// Handle special player entity ID for bot entity in third person
|
|
695
|
+
if (entityPlayerId === 'player_entity' && this.playerEntity?.playerObject) {
|
|
696
|
+
const { playerObject } = this.playerEntity
|
|
697
|
+
if (animation === 'oneSwing') {
|
|
698
|
+
if (playerObject.animation && (playerObject.animation as any).swingArm) {
|
|
699
|
+
(playerObject.animation as any).swingArm()
|
|
700
|
+
}
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (playerObject.animation && (playerObject.animation as any).switchAnimationCallback !== undefined) {
|
|
705
|
+
(playerObject.animation as any).switchAnimationCallback = () => {
|
|
706
|
+
const anim = playerObject.animation as any
|
|
707
|
+
if (anim) {
|
|
708
|
+
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
709
|
+
anim.isRunning = animation === 'running'
|
|
710
|
+
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Handle regular entities
|
|
718
|
+
const playerObject = this.getPlayerObject(entityPlayerId)
|
|
719
|
+
if (playerObject) {
|
|
720
|
+
if (animation === 'oneSwing') {
|
|
721
|
+
if (playerObject.animation && (playerObject.animation as any).swingArm) {
|
|
722
|
+
(playerObject.animation as any).swingArm()
|
|
723
|
+
}
|
|
724
|
+
return
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (playerObject.animation && (playerObject.animation as any).switchAnimationCallback !== undefined) {
|
|
728
|
+
(playerObject.animation as any).switchAnimationCallback = () => {
|
|
729
|
+
const anim = playerObject.animation as any
|
|
730
|
+
if (anim) {
|
|
731
|
+
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
732
|
+
anim.isRunning = animation === 'running'
|
|
733
|
+
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Handle player entity (for third person view) - fallback for backwards compatibility
|
|
741
|
+
if (this.playerEntity?.playerObject) {
|
|
742
|
+
const { playerObject: playerEntityObject } = this.playerEntity
|
|
743
|
+
if (animation === 'oneSwing') {
|
|
744
|
+
if (playerEntityObject.animation && (playerEntityObject.animation as any).swingArm) {
|
|
745
|
+
(playerEntityObject.animation as any).swingArm()
|
|
746
|
+
}
|
|
747
|
+
return
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (playerEntityObject.animation && (playerEntityObject.animation as any).switchAnimationCallback !== undefined) {
|
|
751
|
+
(playerEntityObject.animation as any).switchAnimationCallback = () => {
|
|
752
|
+
const anim = playerEntityObject.animation as any
|
|
753
|
+
if (anim) {
|
|
754
|
+
anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
|
|
755
|
+
anim.isRunning = animation === 'running'
|
|
756
|
+
anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
parseEntityLabel(jsonLike) {
|
|
764
|
+
if (!jsonLike) return
|
|
765
|
+
try {
|
|
766
|
+
if (jsonLike.type === 'string') {
|
|
767
|
+
return jsonLike.value
|
|
768
|
+
}
|
|
769
|
+
const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike)
|
|
770
|
+
const text = flat(parsed).map(this.textFromComponent)
|
|
771
|
+
return text.join('')
|
|
772
|
+
} catch (err) {
|
|
773
|
+
return jsonLike
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private textFromComponent(component) {
|
|
778
|
+
return typeof component === 'string' ? component : component.text ?? ''
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
getItemMesh(item, specificProps: ItemSpecificContextProperties, faceCamera = false, previousModel?: string) {
|
|
782
|
+
if (!item.nbt && item.nbtData) item.nbt = item.nbtData
|
|
783
|
+
const textureUv = this.worldRenderer.getItemRenderData(item, specificProps)
|
|
784
|
+
if (previousModel && previousModel === textureUv?.modelName) return undefined
|
|
785
|
+
|
|
786
|
+
if (textureUv && 'resolvedModel' in textureUv) {
|
|
787
|
+
const mesh = getBlockMeshFromModel(this.worldRenderer.material, textureUv.resolvedModel, textureUv.modelName, this.worldRenderer.resourcesManager.currentResources.worldBlockProvider!)
|
|
788
|
+
let SCALE = 1
|
|
789
|
+
if (specificProps['minecraft:display_context'] === 'ground') {
|
|
790
|
+
SCALE = 0.5
|
|
791
|
+
} else if (specificProps['minecraft:display_context'] === 'thirdperson') {
|
|
792
|
+
SCALE = 6
|
|
793
|
+
}
|
|
794
|
+
mesh.scale.set(SCALE, SCALE, SCALE)
|
|
795
|
+
const outerGroup = new THREE.Group()
|
|
796
|
+
outerGroup.add(mesh)
|
|
797
|
+
return {
|
|
798
|
+
mesh: outerGroup,
|
|
799
|
+
isBlock: true,
|
|
800
|
+
modelName: textureUv.modelName,
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Render proper 3D model for items
|
|
805
|
+
if (textureUv) {
|
|
806
|
+
const textureThree = textureUv.renderInfo?.texture === 'blocks' ? this.worldRenderer.material.map! : this.worldRenderer.itemsTexture
|
|
807
|
+
const { u, v, su, sv } = textureUv
|
|
808
|
+
const sizeX = su ?? 1 // su is actually width
|
|
809
|
+
const sizeY = sv ?? 1 // sv is actually height
|
|
810
|
+
|
|
811
|
+
// Use the new unified item mesh function
|
|
812
|
+
const result = createItemMesh(textureThree, {
|
|
813
|
+
u,
|
|
814
|
+
v,
|
|
815
|
+
sizeX,
|
|
816
|
+
sizeY
|
|
817
|
+
}, {
|
|
818
|
+
faceCamera,
|
|
819
|
+
use3D: !faceCamera, // Only use 3D for non-camera-facing items
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
let SCALE = 1
|
|
823
|
+
if (specificProps['minecraft:display_context'] === 'ground') {
|
|
824
|
+
SCALE = 0.5
|
|
825
|
+
} else if (specificProps['minecraft:display_context'] === 'thirdperson') {
|
|
826
|
+
SCALE = 6
|
|
827
|
+
}
|
|
828
|
+
result.mesh.scale.set(SCALE, SCALE, SCALE)
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
mesh: result.mesh,
|
|
832
|
+
isBlock: false,
|
|
833
|
+
modelName: textureUv.modelName,
|
|
834
|
+
cleanup: result.cleanup
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
setVisible(mesh: THREE.Object3D, visible: boolean) {
|
|
840
|
+
//mesh.visible = visible
|
|
841
|
+
//TODO: Fix workaround for visibility setting
|
|
842
|
+
if (visible) {
|
|
843
|
+
mesh.scale.set(1, 1, 1)
|
|
844
|
+
} else {
|
|
845
|
+
mesh.scale.set(0, 0, 0)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
update(entity: SceneEntity['originalEntity'], overrides) {
|
|
850
|
+
const isPlayerModel = entity.name === 'player'
|
|
851
|
+
if (entity.name === 'zombie_villager' || entity.name === 'husk') {
|
|
852
|
+
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
|
853
|
+
}
|
|
854
|
+
if (entity.name === 'glow_item_frame') {
|
|
855
|
+
if (!overrides.textures) overrides.textures = []
|
|
856
|
+
overrides.textures['background'] = 'block:glow_item_frame'
|
|
857
|
+
}
|
|
858
|
+
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
|
859
|
+
let e = this.entities[entity.id]
|
|
860
|
+
const justAdded = !e
|
|
861
|
+
|
|
862
|
+
if (entity.delete) {
|
|
863
|
+
if (!e) return
|
|
864
|
+
if (e.additionalCleanup) e.additionalCleanup()
|
|
865
|
+
e.traverse(c => {
|
|
866
|
+
if (c['additionalCleanup']) c['additionalCleanup']()
|
|
867
|
+
})
|
|
868
|
+
this.onRemoveEntity(entity)
|
|
869
|
+
this.worldRenderer.scene.remove(e)
|
|
870
|
+
disposeObject(e)
|
|
871
|
+
// todo dispose textures as well ?
|
|
872
|
+
delete this.entities[entity.id]
|
|
873
|
+
return
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
let mesh: THREE.Object3D | undefined
|
|
877
|
+
if (e === undefined) {
|
|
878
|
+
this.beforeEntityAdded(entity)
|
|
879
|
+
|
|
880
|
+
const group = new THREE.Group() as unknown as SceneEntity
|
|
881
|
+
group.originalEntity = entity
|
|
882
|
+
if (entity.name === 'item' || entity.name === 'tnt' || entity.name === 'falling_block' || entity.name === 'snowball'
|
|
883
|
+
|| entity.name === 'egg' || entity.name === 'ender_pearl' || entity.name === 'experience_bottle'
|
|
884
|
+
|| entity.name === 'splash_potion' || entity.name === 'lingering_potion') {
|
|
885
|
+
const item = entity.name === 'tnt' || entity.type === 'projectile'
|
|
886
|
+
? { name: entity.name }
|
|
887
|
+
: entity.name === 'falling_block'
|
|
888
|
+
? { blockState: entity['objectData'] }
|
|
889
|
+
: entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
|
890
|
+
if (item) {
|
|
891
|
+
const object = this.getItemMesh(item, {
|
|
892
|
+
'minecraft:display_context': 'ground',
|
|
893
|
+
}, entity.type === 'projectile')
|
|
894
|
+
if (object) {
|
|
895
|
+
mesh = object.mesh
|
|
896
|
+
if (entity.name === 'item' || entity.type === 'projectile') {
|
|
897
|
+
mesh.scale.set(0.5, 0.5, 0.5)
|
|
898
|
+
mesh.position.set(0, entity.name === 'item' ? 0.2 : 0.1, 0)
|
|
899
|
+
} else {
|
|
900
|
+
mesh.scale.set(2, 2, 2)
|
|
901
|
+
mesh.position.set(0, 0.5, 0)
|
|
902
|
+
}
|
|
903
|
+
// set faces
|
|
904
|
+
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
|
|
905
|
+
// viewer.scene.add(mesh)
|
|
906
|
+
if (entity.name === 'item') {
|
|
907
|
+
const clock = new THREE.Clock()
|
|
908
|
+
mesh.onBeforeRender = () => {
|
|
909
|
+
const delta = clock.getDelta()
|
|
910
|
+
mesh!.rotation.y += delta
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// TNT blinking
|
|
915
|
+
// if (entity.name === 'tnt') {
|
|
916
|
+
// let lastBlink = 0
|
|
917
|
+
// const blinkInterval = 500 // ms between blinks
|
|
918
|
+
// mesh.onBeforeRender = () => {
|
|
919
|
+
// const now = Date.now()
|
|
920
|
+
// if (now - lastBlink > blinkInterval) {
|
|
921
|
+
// lastBlink = now
|
|
922
|
+
// mesh.traverse((child) => {
|
|
923
|
+
// if (child instanceof THREE.Mesh) {
|
|
924
|
+
// const material = child.material as THREE.MeshLambertMaterial
|
|
925
|
+
// material.color.set(material.color?.equals(new THREE.Color(0xff_ff_ff))
|
|
926
|
+
// ? new THREE.Color(0xff_00_00)
|
|
927
|
+
// : new THREE.Color(0xff_ff_ff))
|
|
928
|
+
// }
|
|
929
|
+
// })
|
|
930
|
+
// }
|
|
931
|
+
// }
|
|
932
|
+
// }
|
|
933
|
+
|
|
934
|
+
group.additionalCleanup = () => {
|
|
935
|
+
// important: avoid texture memory leak and gpu slowdown
|
|
936
|
+
if (object.cleanup) {
|
|
937
|
+
object.cleanup()
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} else if (isPlayerModel) {
|
|
943
|
+
const wrapper = new THREE.Group()
|
|
944
|
+
const playerObject = this.setupPlayerObject(entity, wrapper, overrides)
|
|
945
|
+
group.playerObject = playerObject
|
|
946
|
+
mesh = wrapper
|
|
947
|
+
|
|
948
|
+
if (entity.username) {
|
|
949
|
+
const nametag = addNametag(entity, { fontFamily: 'mojangles' }, wrapper, this.worldRenderer.version)
|
|
950
|
+
if (nametag) {
|
|
951
|
+
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
|
952
|
+
nametag.scale.multiplyScalar(12)
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
mesh = getEntityMesh(this.mcData, entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] })
|
|
957
|
+
}
|
|
958
|
+
if (!mesh) return
|
|
959
|
+
mesh.name = 'mesh'
|
|
960
|
+
// set initial position so there are no weird jumps update after
|
|
961
|
+
const pos = entity.pos ?? entity.position
|
|
962
|
+
group.position.set(pos.x, pos.y, pos.z)
|
|
963
|
+
|
|
964
|
+
// todo use width and height instead
|
|
965
|
+
const boxHelper = new THREE.BoxHelper(
|
|
966
|
+
mesh,
|
|
967
|
+
entity.type === 'hostile' ? 0xff_00_00 :
|
|
968
|
+
entity.type === 'mob' ? 0x00_ff_00 :
|
|
969
|
+
entity.type === 'player' ? 0x00_00_ff :
|
|
970
|
+
0xff_a5_00,
|
|
971
|
+
)
|
|
972
|
+
boxHelper.name = 'debug'
|
|
973
|
+
group.add(mesh)
|
|
974
|
+
group.add(boxHelper)
|
|
975
|
+
boxHelper.visible = false
|
|
976
|
+
this.worldRenderer.scene.add(group)
|
|
977
|
+
|
|
978
|
+
e = group
|
|
979
|
+
e.name = 'entity'
|
|
980
|
+
e['realName'] = entity.name
|
|
981
|
+
this.entities[entity.id] = e
|
|
982
|
+
|
|
983
|
+
if (isPlayerModel) {
|
|
984
|
+
void this.updatePlayerSkin(entity.id, entity.username, overrides?.texture ? entity.uuid : undefined, overrides?.texture || stevePngUrl)
|
|
985
|
+
}
|
|
986
|
+
this.setDebugMode(this.debugMode, group)
|
|
987
|
+
this.setRendering(this.currentlyRendering, group)
|
|
988
|
+
|
|
989
|
+
this.afterAddEntity(entity)
|
|
990
|
+
} else {
|
|
991
|
+
mesh = e.children.find(c => c.name === 'mesh')
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Update equipment
|
|
995
|
+
this.updateEntityEquipment(e, entity)
|
|
996
|
+
|
|
997
|
+
const meta = getGeneralEntitiesMetadata(entity, this.mcData)
|
|
998
|
+
|
|
999
|
+
const isInvisible = ((entity.metadata?.[0] ?? 0) as unknown as number) & 0x20 || (this.worldRenderer.playerStateReactive.cameraSpectatingEntity === entity.id && this.worldRenderer.playerStateUtils.isSpectator())
|
|
1000
|
+
for (const child of mesh!.children ?? []) {
|
|
1001
|
+
if (child.name !== 'nametag') {
|
|
1002
|
+
child.visible = !isInvisible
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// ---
|
|
1006
|
+
// set baby size
|
|
1007
|
+
if (meta.baby) {
|
|
1008
|
+
e.scale.set(0.5, 0.5, 0.5)
|
|
1009
|
+
} else {
|
|
1010
|
+
e.scale.set(1, 1, 1)
|
|
1011
|
+
}
|
|
1012
|
+
// entity specific meta
|
|
1013
|
+
const textDisplayMeta = getSpecificEntityMetadata('text_display', entity, this.mcData)
|
|
1014
|
+
const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name
|
|
1015
|
+
if (entity.name !== 'player' && displayTextRaw) {
|
|
1016
|
+
const nameTagFixed = textDisplayMeta && (textDisplayMeta.billboard_render_constraints === 'fixed' || !textDisplayMeta.billboard_render_constraints)
|
|
1017
|
+
const nameTagBackgroundColor = (textDisplayMeta && (parseInt(textDisplayMeta.style_flags, 10) & 0x04) === 0) ? toRgba(textDisplayMeta.background_color) : undefined
|
|
1018
|
+
let nameTagTextOpacity: any
|
|
1019
|
+
if (textDisplayMeta?.text_opacity) {
|
|
1020
|
+
const rawOpacity = parseInt(textDisplayMeta?.text_opacity, 10)
|
|
1021
|
+
nameTagTextOpacity = rawOpacity > 0 ? rawOpacity : 256 - rawOpacity
|
|
1022
|
+
}
|
|
1023
|
+
addNametag(
|
|
1024
|
+
{
|
|
1025
|
+
...entity, username: typeof displayTextRaw === 'string' ? mojangson.simplify(mojangson.parse(displayTextRaw)) : nbt.simplify(displayTextRaw),
|
|
1026
|
+
nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed,
|
|
1027
|
+
nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)),
|
|
1028
|
+
nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation)
|
|
1029
|
+
},
|
|
1030
|
+
this.entitiesOptions,
|
|
1031
|
+
mesh,
|
|
1032
|
+
this.worldRenderer.version
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const armorStandMeta = getSpecificEntityMetadata('armor_stand', entity, this.mcData)
|
|
1037
|
+
if (armorStandMeta) {
|
|
1038
|
+
const isSmall = (parseInt(armorStandMeta.client_flags, 10) & 0x01) !== 0
|
|
1039
|
+
const hasArms = (parseInt(armorStandMeta.client_flags, 10) & 0x04) !== 0
|
|
1040
|
+
const hasBasePlate = (parseInt(armorStandMeta.client_flags, 10) & 0x08) === 0
|
|
1041
|
+
const isMarker = (parseInt(armorStandMeta.client_flags, 10) & 0x10) !== 0
|
|
1042
|
+
mesh!.castShadow = !isMarker
|
|
1043
|
+
mesh!.receiveShadow = !isMarker
|
|
1044
|
+
if (isSmall) {
|
|
1045
|
+
e.scale.set(0.5, 0.5, 0.5)
|
|
1046
|
+
} else {
|
|
1047
|
+
e.scale.set(1, 1, 1)
|
|
1048
|
+
}
|
|
1049
|
+
e.traverse(c => {
|
|
1050
|
+
switch (c.name) {
|
|
1051
|
+
case 'bone_baseplate':
|
|
1052
|
+
this.setVisible(c, hasBasePlate)
|
|
1053
|
+
c.rotation.y = -e.rotation.y
|
|
1054
|
+
break
|
|
1055
|
+
case 'bone_head':
|
|
1056
|
+
if (armorStandMeta.head_pose) {
|
|
1057
|
+
c.setRotationFromEuler(poseToEuler(armorStandMeta.head_pose))
|
|
1058
|
+
}
|
|
1059
|
+
break
|
|
1060
|
+
case 'bone_body':
|
|
1061
|
+
if (armorStandMeta.body_pose) {
|
|
1062
|
+
c.setRotationFromEuler(poseToEuler(armorStandMeta.body_pose))
|
|
1063
|
+
}
|
|
1064
|
+
break
|
|
1065
|
+
case 'bone_rightarm':
|
|
1066
|
+
if (c.parent?.name !== 'bone_armor') {
|
|
1067
|
+
this.setVisible(c, hasArms)
|
|
1068
|
+
}
|
|
1069
|
+
if (armorStandMeta.left_arm_pose) {
|
|
1070
|
+
c.setRotationFromEuler(poseToEuler(armorStandMeta.left_arm_pose))
|
|
1071
|
+
} else {
|
|
1072
|
+
c.setRotationFromEuler(poseToEuler({ 'yaw': -10, 'pitch': -10, 'roll': 0 }))
|
|
1073
|
+
}
|
|
1074
|
+
break
|
|
1075
|
+
case 'bone_leftarm':
|
|
1076
|
+
if (c.parent?.name !== 'bone_armor') {
|
|
1077
|
+
this.setVisible(c, hasArms)
|
|
1078
|
+
}
|
|
1079
|
+
if (armorStandMeta.right_arm_pose) {
|
|
1080
|
+
c.setRotationFromEuler(poseToEuler(armorStandMeta.right_arm_pose))
|
|
1081
|
+
} else {
|
|
1082
|
+
c.setRotationFromEuler(poseToEuler({ 'yaw': 10, 'pitch': -10, 'roll': 0 }))
|
|
1083
|
+
}
|
|
1084
|
+
break
|
|
1085
|
+
case 'bone_rightleg':
|
|
1086
|
+
if (armorStandMeta.left_leg_pose) {
|
|
1087
|
+
c.setRotationFromEuler(poseToEuler(armorStandMeta.left_leg_pose))
|
|
1088
|
+
} else {
|
|
1089
|
+
c.setRotationFromEuler(poseToEuler({ 'yaw': -1, 'pitch': -1, 'roll': 0 }))
|
|
1090
|
+
}
|
|
1091
|
+
break
|
|
1092
|
+
case 'bone_leftleg':
|
|
1093
|
+
if (armorStandMeta.right_leg_pose) {
|
|
1094
|
+
c.setRotationFromEuler(poseToEuler(armorStandMeta.right_leg_pose))
|
|
1095
|
+
} else {
|
|
1096
|
+
c.setRotationFromEuler(poseToEuler({ 'yaw': 1, 'pitch': 1, 'roll': 0 }))
|
|
1097
|
+
}
|
|
1098
|
+
break
|
|
1099
|
+
}
|
|
1100
|
+
})
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// todo handle map, map_chunks events
|
|
1104
|
+
let itemFrameMeta = getSpecificEntityMetadata('item_frame', entity, this.mcData)
|
|
1105
|
+
if (!itemFrameMeta) {
|
|
1106
|
+
itemFrameMeta = getSpecificEntityMetadata('glow_item_frame', entity, this.mcData)
|
|
1107
|
+
}
|
|
1108
|
+
if (itemFrameMeta) {
|
|
1109
|
+
// TODO: fix type
|
|
1110
|
+
// todo! fix errors in mc-data (no entities data prior 1.18.2)
|
|
1111
|
+
const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } }
|
|
1112
|
+
mesh!.scale.set(1, 1, 1)
|
|
1113
|
+
mesh!.position.set(0, 0, -0.5)
|
|
1114
|
+
|
|
1115
|
+
e.rotation.x = -entity.pitch
|
|
1116
|
+
e.children.find(c => {
|
|
1117
|
+
if (c.name.startsWith('map_')) {
|
|
1118
|
+
disposeObject(c)
|
|
1119
|
+
const existingMapNumber = parseInt(c.name.split('_')[1], 10)
|
|
1120
|
+
this.itemFrameMaps[existingMapNumber] = this.itemFrameMaps[existingMapNumber]?.filter(mesh => mesh !== c)
|
|
1121
|
+
if (c instanceof THREE.Mesh) {
|
|
1122
|
+
c.material?.map?.dispose()
|
|
1123
|
+
}
|
|
1124
|
+
return true
|
|
1125
|
+
} else if (c.name === 'item') {
|
|
1126
|
+
disposeObject(c)
|
|
1127
|
+
return true
|
|
1128
|
+
}
|
|
1129
|
+
return false
|
|
1130
|
+
})?.removeFromParent()
|
|
1131
|
+
|
|
1132
|
+
if (item && (item.itemId ?? item.blockId ?? 0) !== 0) {
|
|
1133
|
+
// Get rotation from metadata, default to 0 if not present
|
|
1134
|
+
// Rotation is stored in 45° increments (0-7) for items, 90° increments (0-3) for maps
|
|
1135
|
+
const rotation = (itemFrameMeta.rotation as any as number) ?? 0
|
|
1136
|
+
const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data
|
|
1137
|
+
if (mapNumber) {
|
|
1138
|
+
// TODO: Use proper larger item frame model when a map exists
|
|
1139
|
+
mesh!.scale.set(16 / 12, 16 / 12, 1)
|
|
1140
|
+
// Handle map rotation (4 possibilities, 90° increments)
|
|
1141
|
+
this.addMapModel(e, mapNumber, rotation)
|
|
1142
|
+
} else {
|
|
1143
|
+
// Handle regular item rotation (8 possibilities, 45° increments)
|
|
1144
|
+
const itemMesh = this.getItemMesh(item, {
|
|
1145
|
+
'minecraft:display_context': 'fixed',
|
|
1146
|
+
})
|
|
1147
|
+
if (itemMesh) {
|
|
1148
|
+
itemMesh.mesh.position.set(0, 0, -0.05)
|
|
1149
|
+
// itemMesh.mesh.position.set(0, 0, 0.43)
|
|
1150
|
+
if (itemMesh.isBlock) {
|
|
1151
|
+
itemMesh.mesh.scale.set(0.25, 0.25, 0.25)
|
|
1152
|
+
} else {
|
|
1153
|
+
itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
|
|
1154
|
+
}
|
|
1155
|
+
// Rotate 180° around Y axis first
|
|
1156
|
+
itemMesh.mesh.rotateY(Math.PI)
|
|
1157
|
+
// Then apply the 45° increment rotation
|
|
1158
|
+
itemMesh.mesh.rotateZ(-rotation * Math.PI / 4)
|
|
1159
|
+
itemMesh.mesh.name = 'item'
|
|
1160
|
+
e.add(itemMesh.mesh)
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (entity.username !== undefined) {
|
|
1167
|
+
e.username = entity.username
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
this.updateNameTagVisibility(e)
|
|
1171
|
+
|
|
1172
|
+
this.updateEntityPosition(entity, justAdded, overrides)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
updateEntityPosition(entity: import('prismarine-entity').Entity, justAdded: boolean, overrides: { rotation?: { head?: { y: number, x: number } } }) {
|
|
1176
|
+
const e = this.entities[entity.id]
|
|
1177
|
+
if (!e) return
|
|
1178
|
+
const ANIMATION_DURATION = justAdded ? 0 : TWEEN_DURATION
|
|
1179
|
+
if (entity.position) {
|
|
1180
|
+
new TWEEN.Tween(e.position).to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION).start()
|
|
1181
|
+
}
|
|
1182
|
+
if (entity.yaw) {
|
|
1183
|
+
const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
|
|
1184
|
+
const dy = 2 * da % (Math.PI * 2) - da
|
|
1185
|
+
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, ANIMATION_DURATION).start()
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (e?.playerObject && overrides?.rotation?.head) {
|
|
1189
|
+
const { playerObject } = e
|
|
1190
|
+
const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0
|
|
1191
|
+
playerObject.skin.head.rotation.y = -headRotationDiff
|
|
1192
|
+
playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
afterAddEntity(entity: import('prismarine-entity').Entity) {
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
beforeEntityAdded(entity: import('prismarine-entity').Entity) {
|
|
1200
|
+
const override = this.pendingModelOverrides.get(entity.id.toString())
|
|
1201
|
+
if (override) {
|
|
1202
|
+
const { modelPath, modelType, metadata } = override
|
|
1203
|
+
entity['customModel'] = { modelPath, modelType, metadata }
|
|
1204
|
+
this.pendingModelOverrides.delete(entity.id.toString())
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
loadedSkinEntityIds = new Set<string>()
|
|
1209
|
+
maybeRenderPlayerSkin(entityId: string) {
|
|
1210
|
+
let mesh = this.entities[entityId]
|
|
1211
|
+
if (entityId === 'player_entity') {
|
|
1212
|
+
mesh = this.playerEntity!
|
|
1213
|
+
entityId = this.playerEntity?.originalEntity.id as any
|
|
1214
|
+
}
|
|
1215
|
+
if (!mesh) return
|
|
1216
|
+
if (!mesh.playerObject) return
|
|
1217
|
+
if (!mesh.visible) return
|
|
1218
|
+
|
|
1219
|
+
const MAX_DISTANCE_SKIN_LOAD = 128
|
|
1220
|
+
const cameraPos = this.worldRenderer.cameraObject.position
|
|
1221
|
+
const distance = mesh.position.distanceTo(cameraPos)
|
|
1222
|
+
if (distance < MAX_DISTANCE_SKIN_LOAD && distance < (this.worldRenderer.viewDistance * 16)) {
|
|
1223
|
+
if (this.loadedSkinEntityIds.has(String(entityId))) return
|
|
1224
|
+
void this.updatePlayerSkin(entityId, mesh.playerObject.realUsername, mesh.playerObject.realPlayerUuid, true, true)
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
playerPerAnimation = {} as Record<number, string>
|
|
1229
|
+
onRemoveEntity(entity: import('prismarine-entity').Entity) {
|
|
1230
|
+
this.loadedSkinEntityIds.delete(entity.id.toString())
|
|
1231
|
+
delete this.currentSkinUrls[entity.id.toString()]
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
updateMap(mapNumber: string | number, data: string) {
|
|
1235
|
+
this.cachedMapsImages[mapNumber] = data
|
|
1236
|
+
let itemFrameMeshes = this.itemFrameMaps[mapNumber]
|
|
1237
|
+
if (!itemFrameMeshes) return
|
|
1238
|
+
itemFrameMeshes = itemFrameMeshes.filter(mesh => mesh.parent)
|
|
1239
|
+
this.itemFrameMaps[mapNumber] = itemFrameMeshes
|
|
1240
|
+
if (itemFrameMeshes) {
|
|
1241
|
+
for (const mesh of itemFrameMeshes) {
|
|
1242
|
+
mesh.material.map = this.loadMap(data)
|
|
1243
|
+
mesh.material.needsUpdate = true
|
|
1244
|
+
mesh.visible = true
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
updateNameTagVisibility(entity: SceneEntity) {
|
|
1250
|
+
const playerTeam = this.worldRenderer.playerStateReactive.team
|
|
1251
|
+
const entityTeam = entity.originalEntity.team
|
|
1252
|
+
const nameTagVisibility = entityTeam?.nameTagVisibility || 'always'
|
|
1253
|
+
const showNameTag = nameTagVisibility === 'always' ||
|
|
1254
|
+
(nameTagVisibility === 'hideForOwnTeam' && entityTeam?.team !== playerTeam?.team) ||
|
|
1255
|
+
(nameTagVisibility === 'hideForOtherTeams' && (entityTeam?.team === playerTeam?.team || playerTeam === undefined))
|
|
1256
|
+
entity.traverse(c => {
|
|
1257
|
+
if (c.name === 'nametag') {
|
|
1258
|
+
c.visible = showNameTag
|
|
1259
|
+
}
|
|
1260
|
+
})
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
addMapModel(entityMesh: THREE.Object3D, mapNumber: number, rotation: number) {
|
|
1264
|
+
const imageData = this.cachedMapsImages?.[mapNumber]
|
|
1265
|
+
let texture: THREE.Texture | null = null
|
|
1266
|
+
if (imageData) {
|
|
1267
|
+
texture = this.loadMap(imageData)
|
|
1268
|
+
}
|
|
1269
|
+
const parameters = {
|
|
1270
|
+
transparent: true,
|
|
1271
|
+
alphaTest: 0.1,
|
|
1272
|
+
}
|
|
1273
|
+
if (texture) {
|
|
1274
|
+
parameters['map'] = texture
|
|
1275
|
+
}
|
|
1276
|
+
const material = new THREE.MeshLambertMaterial(parameters)
|
|
1277
|
+
|
|
1278
|
+
const mapMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material)
|
|
1279
|
+
|
|
1280
|
+
mapMesh.rotation.set(0, Math.PI, 0)
|
|
1281
|
+
entityMesh.add(mapMesh)
|
|
1282
|
+
let isInvisible = true
|
|
1283
|
+
entityMesh.traverseVisible(c => {
|
|
1284
|
+
if (c.name === 'geometry_frame') {
|
|
1285
|
+
isInvisible = false
|
|
1286
|
+
}
|
|
1287
|
+
})
|
|
1288
|
+
if (isInvisible) {
|
|
1289
|
+
mapMesh.position.set(0, 0, 0.499)
|
|
1290
|
+
} else {
|
|
1291
|
+
mapMesh.position.set(0, 0, 0.437)
|
|
1292
|
+
}
|
|
1293
|
+
// Apply 90° increment rotation for maps (0-3)
|
|
1294
|
+
mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
|
|
1295
|
+
mapMesh.name = `map_${mapNumber}`
|
|
1296
|
+
|
|
1297
|
+
if (!texture) {
|
|
1298
|
+
mapMesh.visible = false
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (!this.itemFrameMaps[mapNumber]) {
|
|
1302
|
+
this.itemFrameMaps[mapNumber] = []
|
|
1303
|
+
}
|
|
1304
|
+
this.itemFrameMaps[mapNumber].push(mapMesh)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
loadMap(data: any) {
|
|
1308
|
+
const texture = new THREE.TextureLoader().load(data)
|
|
1309
|
+
if (texture) {
|
|
1310
|
+
texture.magFilter = THREE.NearestFilter
|
|
1311
|
+
texture.minFilter = THREE.NearestFilter
|
|
1312
|
+
texture.needsUpdate = true
|
|
1313
|
+
}
|
|
1314
|
+
return texture
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
addItemModel(entityMesh: SceneEntity, hand: 'left' | 'right', item: Item, isPlayer = false) {
|
|
1318
|
+
const bedrockParentName = `bone_${hand}item`
|
|
1319
|
+
const itemName = `custom_item_${hand}`
|
|
1320
|
+
|
|
1321
|
+
// remove existing item
|
|
1322
|
+
entityMesh.traverse(c => {
|
|
1323
|
+
if (c.name === itemName) {
|
|
1324
|
+
c.removeFromParent()
|
|
1325
|
+
if (c['additionalCleanup']) c['additionalCleanup']()
|
|
1326
|
+
}
|
|
1327
|
+
})
|
|
1328
|
+
if (!item) return
|
|
1329
|
+
|
|
1330
|
+
const itemObject = this.getItemMesh(item, {
|
|
1331
|
+
'minecraft:display_context': 'thirdperson',
|
|
1332
|
+
})
|
|
1333
|
+
if (itemObject?.mesh) {
|
|
1334
|
+
entityMesh.traverse(c => {
|
|
1335
|
+
if (c.name.toLowerCase() === bedrockParentName || c.name === `${hand}Arm`) {
|
|
1336
|
+
const group = new THREE.Object3D()
|
|
1337
|
+
group['additionalCleanup'] = () => {
|
|
1338
|
+
// important: avoid texture memory leak and gpu slowdown
|
|
1339
|
+
if (itemObject.cleanup) {
|
|
1340
|
+
itemObject.cleanup()
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const itemMesh = itemObject.mesh
|
|
1344
|
+
group.rotation.z = -Math.PI / 16
|
|
1345
|
+
if (itemObject.isBlock) {
|
|
1346
|
+
group.rotation.y = Math.PI / 4
|
|
1347
|
+
} else {
|
|
1348
|
+
itemMesh.rotation.z = -Math.PI / 4
|
|
1349
|
+
group.rotation.y = Math.PI / 2
|
|
1350
|
+
group.scale.multiplyScalar(2)
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// if player, move item below and forward a bit
|
|
1354
|
+
if (isPlayer) {
|
|
1355
|
+
group.position.y = -8
|
|
1356
|
+
group.position.z = 5
|
|
1357
|
+
group.position.x = hand === 'left' ? 1 : -1
|
|
1358
|
+
group.rotation.x = Math.PI
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
group.add(itemMesh)
|
|
1362
|
+
|
|
1363
|
+
group.name = itemName
|
|
1364
|
+
c.add(group)
|
|
1365
|
+
}
|
|
1366
|
+
})
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
handleDamageEvent(entityId, damageAmount) {
|
|
1371
|
+
const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
|
|
1372
|
+
if (entityMesh) {
|
|
1373
|
+
entityMesh.traverse((child) => {
|
|
1374
|
+
if (child instanceof THREE.Mesh && child.material.clone) {
|
|
1375
|
+
const clonedMaterial = child.material.clone()
|
|
1376
|
+
clonedMaterial.dispose()
|
|
1377
|
+
child.material = child.material.clone()
|
|
1378
|
+
const originalColor = child.material.color.clone()
|
|
1379
|
+
child.material.color.set(0xff_00_00)
|
|
1380
|
+
new TWEEN.Tween(child.material.color)
|
|
1381
|
+
.to(originalColor, 500)
|
|
1382
|
+
.start()
|
|
1383
|
+
}
|
|
1384
|
+
})
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
raycastSceneDebug() {
|
|
1389
|
+
// return any object from scene. raycast from camera
|
|
1390
|
+
const raycaster = new THREE.Raycaster()
|
|
1391
|
+
raycaster.setFromCamera(new THREE.Vector2(0, 0), this.worldRenderer.camera)
|
|
1392
|
+
const intersects = raycaster.intersectObjects(this.worldRenderer.scene.children)
|
|
1393
|
+
return intersects[0]?.object
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
updateEntityModel(entityId: string, modelPath: string, modelType: Entity.EntityModelType, metadata?: any) {
|
|
1397
|
+
// Store override data for future entities
|
|
1398
|
+
this.pendingModelOverrides.set(entityId, { modelPath, modelType, metadata })
|
|
1399
|
+
|
|
1400
|
+
// Force entity recreation if it exists
|
|
1401
|
+
const entity = this.entities[entityId]
|
|
1402
|
+
if (!entity) return
|
|
1403
|
+
|
|
1404
|
+
// Update with remove flag to force recreation
|
|
1405
|
+
this.update({ ...entity.originalEntity, delete: true } as SceneEntity['originalEntity'], {})
|
|
1406
|
+
this.update(entity.originalEntity, {})
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
private setupPlayerObject(entity: SceneEntity['originalEntity'], wrapper: THREE.Group, overrides: { texture?: string }): PlayerObjectType {
|
|
1410
|
+
const playerObject = new PlayerObject() as PlayerObjectType
|
|
1411
|
+
playerObject.realPlayerUuid = entity.uuid ?? ''
|
|
1412
|
+
playerObject.realUsername = entity.username ?? ''
|
|
1413
|
+
playerObject.position.set(0, 16, 0)
|
|
1414
|
+
|
|
1415
|
+
// fix issues with starfield
|
|
1416
|
+
playerObject.traverse((obj) => {
|
|
1417
|
+
if (obj instanceof THREE.Mesh && obj.material instanceof THREE.MeshStandardMaterial) {
|
|
1418
|
+
obj.material.transparent = true
|
|
1419
|
+
}
|
|
1420
|
+
})
|
|
1421
|
+
|
|
1422
|
+
wrapper.add(playerObject as any)
|
|
1423
|
+
const scale = 1 / 16
|
|
1424
|
+
wrapper.scale.set(scale, scale, scale)
|
|
1425
|
+
wrapper.rotation.set(0, Math.PI, 0)
|
|
1426
|
+
|
|
1427
|
+
// Set up animation
|
|
1428
|
+
playerObject.animation = new WalkingGeneralSwing()
|
|
1429
|
+
//@ts-expect-error
|
|
1430
|
+
playerObject.animation.isMoving = false
|
|
1431
|
+
|
|
1432
|
+
return playerObject
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
private updateEntityEquipment(entityMesh: SceneEntity, entity: SceneEntity['originalEntity']) {
|
|
1436
|
+
if (!entityMesh || !entity.equipment) return
|
|
1437
|
+
|
|
1438
|
+
const isPlayer = entity.type === 'player'
|
|
1439
|
+
this.addItemModel(entityMesh, isPlayer ? 'right' : 'left', entity.equipment[0], isPlayer)
|
|
1440
|
+
this.addItemModel(entityMesh, isPlayer ? 'left' : 'right', entity.equipment[1], isPlayer)
|
|
1441
|
+
addArmorModel(this.worldRenderer, entityMesh, 'feet', entity.equipment[2])
|
|
1442
|
+
addArmorModel(this.worldRenderer, entityMesh, 'legs', entity.equipment[3], 2)
|
|
1443
|
+
addArmorModel(this.worldRenderer, entityMesh, 'chest', entity.equipment[4])
|
|
1444
|
+
addArmorModel(this.worldRenderer, entityMesh, 'head', entity.equipment[5])
|
|
1445
|
+
|
|
1446
|
+
// Update player-specific equipment
|
|
1447
|
+
if (isPlayer && entityMesh.playerObject) {
|
|
1448
|
+
const { playerObject } = entityMesh
|
|
1449
|
+
playerObject.backEquipment = entity.equipment.some((item) => item?.name === 'elytra') ? 'elytra' : 'cape'
|
|
1450
|
+
if (playerObject.backEquipment === 'elytra') {
|
|
1451
|
+
void this.loadAndApplyCape(entity.id, elytraTexture)
|
|
1452
|
+
}
|
|
1453
|
+
if (playerObject.cape.map === null) {
|
|
1454
|
+
playerObject.cape.visible = false
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function getGeneralEntitiesMetadata(entity: { name; metadata }, mcData?: IndexedData): Partial<UnionToIntersection<EntityMetadataVersions[keyof EntityMetadataVersions]>> {
|
|
1461
|
+
const entityData = mcData?.entitiesByName[entity.name]
|
|
1462
|
+
return new Proxy({}, {
|
|
1463
|
+
get(target, p, receiver) {
|
|
1464
|
+
if (typeof p !== 'string' || !entityData) return
|
|
1465
|
+
const index = entityData.metadataKeys?.indexOf(p)
|
|
1466
|
+
return entity.metadata?.[index ?? -1]
|
|
1467
|
+
},
|
|
1468
|
+
})
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function getSpecificEntityMetadata<T extends keyof EntityMetadataVersions>(name: T, entity, mcData?: IndexedData): EntityMetadataVersions[T] | undefined {
|
|
1472
|
+
if (entity.name !== name) return
|
|
1473
|
+
return getGeneralEntitiesMetadata(entity, mcData) as any
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function addArmorModel(worldRenderer: WorldRendererThree, entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) {
|
|
1477
|
+
if (!item) {
|
|
1478
|
+
removeArmorModel(entityMesh, slotType)
|
|
1479
|
+
return
|
|
1480
|
+
}
|
|
1481
|
+
const itemParts = item.name.split('_')
|
|
1482
|
+
let texturePath
|
|
1483
|
+
const isPlayerHead = slotType === 'head' && item.name === 'player_head'
|
|
1484
|
+
if (isPlayerHead) {
|
|
1485
|
+
removeArmorModel(entityMesh, slotType)
|
|
1486
|
+
if (item.nbt) {
|
|
1487
|
+
const itemNbt = nbt.simplify(item.nbt)
|
|
1488
|
+
try {
|
|
1489
|
+
let textureData
|
|
1490
|
+
if (itemNbt.SkullOwner) {
|
|
1491
|
+
textureData = itemNbt.SkullOwner.Properties.textures[0]?.Value
|
|
1492
|
+
} else {
|
|
1493
|
+
textureData = itemNbt['minecraft:profile']?.Properties?.find(p => p.name === 'textures')?.value
|
|
1494
|
+
}
|
|
1495
|
+
if (textureData) {
|
|
1496
|
+
const decodedData = JSON.parse(Buffer.from(textureData, 'base64').toString())
|
|
1497
|
+
texturePath = decodedData.textures?.SKIN?.url
|
|
1498
|
+
const { skinTexturesProxy } = worldRenderer.worldRendererConfig
|
|
1499
|
+
if (skinTexturesProxy) {
|
|
1500
|
+
texturePath = texturePath?.replace('http://textures.minecraft.net/', skinTexturesProxy)
|
|
1501
|
+
.replace('https://textures.minecraft.net/', skinTexturesProxy)
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
console.error('Error decoding player head texture:', err)
|
|
1506
|
+
}
|
|
1507
|
+
} else {
|
|
1508
|
+
texturePath = stevePngUrl
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const armorMaterial = itemParts[0]
|
|
1512
|
+
if (!texturePath) {
|
|
1513
|
+
// TODO: Support mirroring on certain parts of the model
|
|
1514
|
+
const armorTextureName = `${armorMaterial}_layer_${layer}${overlay ? '_overlay' : ''}`
|
|
1515
|
+
texturePath = worldRenderer.resourcesManager.currentResources.customTextures.armor?.textures[armorTextureName]?.src ?? armorTextures[armorTextureName]
|
|
1516
|
+
}
|
|
1517
|
+
if (!texturePath || !armorModel[slotType]) {
|
|
1518
|
+
removeArmorModel(entityMesh, slotType)
|
|
1519
|
+
return
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const meshName = `geometry_armor_${slotType}${overlay ? '_overlay' : ''}`
|
|
1523
|
+
let mesh = entityMesh.children.findLast(c => c.name === meshName) as THREE.Mesh
|
|
1524
|
+
let material
|
|
1525
|
+
if (mesh) {
|
|
1526
|
+
material = mesh.material
|
|
1527
|
+
void loadTexture(texturePath, texture => {
|
|
1528
|
+
texture.magFilter = THREE.NearestFilter
|
|
1529
|
+
texture.minFilter = THREE.NearestFilter
|
|
1530
|
+
texture.flipY = false
|
|
1531
|
+
texture.wrapS = THREE.MirroredRepeatWrapping
|
|
1532
|
+
texture.wrapT = THREE.MirroredRepeatWrapping
|
|
1533
|
+
material.map = texture
|
|
1534
|
+
})
|
|
1535
|
+
} else {
|
|
1536
|
+
mesh = getMesh(worldRenderer, texturePath, armorModel[slotType])
|
|
1537
|
+
// // enable debug mode to see the mesh
|
|
1538
|
+
// mesh.traverse(c => {
|
|
1539
|
+
// if (c instanceof THREE.Mesh) {
|
|
1540
|
+
// c.material.wireframe = true
|
|
1541
|
+
// }
|
|
1542
|
+
// })
|
|
1543
|
+
if (slotType === 'head') {
|
|
1544
|
+
// avoid z-fighting with the head
|
|
1545
|
+
mesh.children[0].position.y += 0.01
|
|
1546
|
+
}
|
|
1547
|
+
mesh.name = meshName
|
|
1548
|
+
material = mesh.material
|
|
1549
|
+
if (!isPlayerHead) {
|
|
1550
|
+
material.side = THREE.DoubleSide
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (armorMaterial === 'leather' && !overlay) {
|
|
1554
|
+
const color = (item.nbt?.value as any)?.display?.value?.color?.value
|
|
1555
|
+
if (color) {
|
|
1556
|
+
const r = color >> 16 & 0xff
|
|
1557
|
+
const g = color >> 8 & 0xff
|
|
1558
|
+
const b = color & 0xff
|
|
1559
|
+
material.color.setRGB(r / 255, g / 255, b / 255)
|
|
1560
|
+
} else {
|
|
1561
|
+
material.color.setHex(0xB5_6D_51) // default brown color
|
|
1562
|
+
}
|
|
1563
|
+
addArmorModel(worldRenderer, entityMesh, slotType, item, layer, true)
|
|
1564
|
+
} else {
|
|
1565
|
+
material.color.setHex(0xFF_FF_FF)
|
|
1566
|
+
}
|
|
1567
|
+
const group = new THREE.Object3D()
|
|
1568
|
+
group.name = `armor_${slotType}${overlay ? '_overlay' : ''}`
|
|
1569
|
+
group.add(mesh)
|
|
1570
|
+
|
|
1571
|
+
entityMesh.add(mesh)
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function removeArmorModel(entityMesh: THREE.Object3D, slotType: string) {
|
|
1575
|
+
for (const c of entityMesh.children) {
|
|
1576
|
+
if (c.name === `geometry_armor_${slotType}` || c.name === `geometry_armor_${slotType}_overlay`) {
|
|
1577
|
+
c.removeFromParent()
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|