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.
Files changed (187) hide show
  1. package/README.md +297 -0
  2. package/dist/index.html +83 -0
  3. package/dist/static/image/arrow.6f27b59f.png +0 -0
  4. package/dist/static/image/blocksAtlasLatest.7850afa3.png +0 -0
  5. package/dist/static/image/blocksAtlasLegacy.5c76823d.png +0 -0
  6. package/dist/static/image/itemsAtlasLatest.36036f95.png +0 -0
  7. package/dist/static/image/itemsAtlasLegacy.dcb1b58d.png +0 -0
  8. package/dist/static/image/tipped_arrow.6f27b59f.png +0 -0
  9. package/dist/static/js/365.f05233ab.js +8462 -0
  10. package/dist/static/js/365.f05233ab.js.LICENSE.txt +52 -0
  11. package/dist/static/js/async/738.efa27644.js +1 -0
  12. package/dist/static/js/index.092ec5be.js +56 -0
  13. package/dist/static/js/lib-polyfill.98986ac5.js +1 -0
  14. package/dist/static/js/lib-react.5c9129e0.js +2 -0
  15. package/dist/static/js/lib-react.5c9129e0.js.LICENSE.txt +39 -0
  16. package/package.json +104 -0
  17. package/src/assets/destroy_stage_0.png +0 -0
  18. package/src/assets/destroy_stage_1.png +0 -0
  19. package/src/assets/destroy_stage_2.png +0 -0
  20. package/src/assets/destroy_stage_3.png +0 -0
  21. package/src/assets/destroy_stage_4.png +0 -0
  22. package/src/assets/destroy_stage_5.png +0 -0
  23. package/src/assets/destroy_stage_6.png +0 -0
  24. package/src/assets/destroy_stage_7.png +0 -0
  25. package/src/assets/destroy_stage_8.png +0 -0
  26. package/src/assets/destroy_stage_9.png +0 -0
  27. package/src/examples/README.md +146 -0
  28. package/src/examples/appViewerExample.ts +205 -0
  29. package/src/examples/initialMenuStart.ts +161 -0
  30. package/src/graphicsBackend/appViewer.ts +297 -0
  31. package/src/graphicsBackend/config.ts +119 -0
  32. package/src/graphicsBackend/index.ts +10 -0
  33. package/src/graphicsBackend/playerState.ts +61 -0
  34. package/src/graphicsBackend/types.ts +143 -0
  35. package/src/index.ts +97 -0
  36. package/src/lib/DebugGui.ts +190 -0
  37. package/src/lib/animationController.ts +85 -0
  38. package/src/lib/buildSharedConfig.mjs +1 -0
  39. package/src/lib/cameraBobbing.ts +94 -0
  40. package/src/lib/canvas2DOverlay.example.ts +361 -0
  41. package/src/lib/canvas2DOverlay.quickstart.ts +242 -0
  42. package/src/lib/canvas2DOverlay.ts +381 -0
  43. package/src/lib/cleanupDecorator.ts +29 -0
  44. package/src/lib/createPlayerObject.ts +55 -0
  45. package/src/lib/frameTimingCollector.ts +164 -0
  46. package/src/lib/guiRenderer.ts +283 -0
  47. package/src/lib/items.ts +140 -0
  48. package/src/lib/mesherlogReader.ts +131 -0
  49. package/src/lib/moreBlockDataGenerated.json +714 -0
  50. package/src/lib/preflatMap.json +1741 -0
  51. package/src/lib/simpleUtils.ts +40 -0
  52. package/src/lib/smoothSwitcher.ts +168 -0
  53. package/src/lib/spiral.ts +29 -0
  54. package/src/lib/ui/newStats.ts +120 -0
  55. package/src/lib/utils/proxy.ts +23 -0
  56. package/src/lib/utils/skins.ts +63 -0
  57. package/src/lib/utils.ts +76 -0
  58. package/src/lib/workerProxy.ts +342 -0
  59. package/src/lib/worldrendererCommon.ts +1088 -0
  60. package/src/mesher/mesher.ts +253 -0
  61. package/src/mesher/models.ts +769 -0
  62. package/src/mesher/modelsGeometryCommon.ts +142 -0
  63. package/src/mesher/shared.ts +80 -0
  64. package/src/mesher/standaloneRenderer.ts +270 -0
  65. package/src/mesher/test/a.ts +3 -0
  66. package/src/mesher/test/mesherTester.ts +76 -0
  67. package/src/mesher/test/playground.ts +19 -0
  68. package/src/mesher/test/test-perf.ts +74 -0
  69. package/src/mesher/test/tests.test.ts +56 -0
  70. package/src/mesher/world.ts +294 -0
  71. package/src/mesher/worldConstants.ts +1 -0
  72. package/src/modules/index.ts +11 -0
  73. package/src/modules/starfield.ts +313 -0
  74. package/src/modules/types.ts +110 -0
  75. package/src/playerState/playerState.ts +78 -0
  76. package/src/playerState/types.ts +36 -0
  77. package/src/playground/allEntitiesDebug.ts +170 -0
  78. package/src/playground/baseScene.ts +587 -0
  79. package/src/playground/mobileControls.tsx +268 -0
  80. package/src/playground/playground.html +83 -0
  81. package/src/playground/playground.ts +11 -0
  82. package/src/playground/playgroundUi.tsx +140 -0
  83. package/src/playground/reactUtils.ts +71 -0
  84. package/src/playground/scenes/allEntities.ts +13 -0
  85. package/src/playground/scenes/entities.ts +37 -0
  86. package/src/playground/scenes/floorRandom.ts +33 -0
  87. package/src/playground/scenes/frequentUpdates.ts +148 -0
  88. package/src/playground/scenes/geometryExport.ts +142 -0
  89. package/src/playground/scenes/index.ts +12 -0
  90. package/src/playground/scenes/lightingStarfield.ts +40 -0
  91. package/src/playground/scenes/main.ts +313 -0
  92. package/src/playground/scenes/railsCobweb.ts +14 -0
  93. package/src/playground/scenes/rotationIssue.ts +7 -0
  94. package/src/playground/scenes/slabsOptimization.ts +16 -0
  95. package/src/playground/scenes/transparencyIssue.ts +11 -0
  96. package/src/playground/shared.ts +79 -0
  97. package/src/resourcesManager/index.ts +5 -0
  98. package/src/resourcesManager/resourcesManager.ts +314 -0
  99. package/src/shims/minecraftData.ts +41 -0
  100. package/src/sign-renderer/index.html +21 -0
  101. package/src/sign-renderer/index.ts +216 -0
  102. package/src/sign-renderer/noop.js +1 -0
  103. package/src/sign-renderer/playground.ts +38 -0
  104. package/src/sign-renderer/tests.test.ts +69 -0
  105. package/src/sign-renderer/vite.config.ts +10 -0
  106. package/src/three/appShared.ts +75 -0
  107. package/src/three/bannerRenderer.ts +275 -0
  108. package/src/three/cameraShake.ts +120 -0
  109. package/src/three/cinimaticScript.ts +350 -0
  110. package/src/three/documentRenderer.ts +491 -0
  111. package/src/three/entities.ts +1580 -0
  112. package/src/three/entity/EntityMesh.ts +707 -0
  113. package/src/three/entity/animations.js +171 -0
  114. package/src/three/entity/armorModels.json +204 -0
  115. package/src/three/entity/armorModels.ts +36 -0
  116. package/src/three/entity/entities.json +6230 -0
  117. package/src/three/entity/exportedModels.js +38 -0
  118. package/src/three/entity/externalTextures.json +1 -0
  119. package/src/three/entity/models/allay.obj +325 -0
  120. package/src/three/entity/models/arrow.obj +60 -0
  121. package/src/three/entity/models/axolotl.obj +509 -0
  122. package/src/three/entity/models/blaze.obj +601 -0
  123. package/src/three/entity/models/boat.obj +417 -0
  124. package/src/three/entity/models/camel.obj +1061 -0
  125. package/src/three/entity/models/cat.obj +509 -0
  126. package/src/three/entity/models/chicken.obj +371 -0
  127. package/src/three/entity/models/cod.obj +371 -0
  128. package/src/three/entity/models/creeper.obj +279 -0
  129. package/src/three/entity/models/dolphin.obj +371 -0
  130. package/src/three/entity/models/ender_dragon.obj +2993 -0
  131. package/src/three/entity/models/enderman.obj +325 -0
  132. package/src/three/entity/models/endermite.obj +187 -0
  133. package/src/three/entity/models/fox.obj +463 -0
  134. package/src/three/entity/models/frog.obj +739 -0
  135. package/src/three/entity/models/ghast.obj +463 -0
  136. package/src/three/entity/models/goat.obj +601 -0
  137. package/src/three/entity/models/guardian.obj +1015 -0
  138. package/src/three/entity/models/horse.obj +1061 -0
  139. package/src/three/entity/models/llama.obj +509 -0
  140. package/src/three/entity/models/minecart.obj +233 -0
  141. package/src/three/entity/models/parrot.obj +509 -0
  142. package/src/three/entity/models/piglin.obj +739 -0
  143. package/src/three/entity/models/pillager.obj +371 -0
  144. package/src/three/entity/models/rabbit.obj +555 -0
  145. package/src/three/entity/models/sheep.obj +555 -0
  146. package/src/three/entity/models/shulker.obj +141 -0
  147. package/src/three/entity/models/sniffer.obj +693 -0
  148. package/src/three/entity/models/spider.obj +509 -0
  149. package/src/three/entity/models/tadpole.obj +95 -0
  150. package/src/three/entity/models/turtle.obj +371 -0
  151. package/src/three/entity/models/vex.obj +325 -0
  152. package/src/three/entity/models/villager.obj +509 -0
  153. package/src/three/entity/models/warden.obj +463 -0
  154. package/src/three/entity/models/witch.obj +647 -0
  155. package/src/three/entity/models/wolf.obj +509 -0
  156. package/src/three/entity/models/zombie_villager.obj +463 -0
  157. package/src/three/entity/objModels.js +1 -0
  158. package/src/three/fireworks.ts +661 -0
  159. package/src/three/fireworksRenderer.ts +434 -0
  160. package/src/three/globals.d.ts +7 -0
  161. package/src/three/graphicsBackend.ts +274 -0
  162. package/src/three/graphicsBackendOffThread.ts +107 -0
  163. package/src/three/hand.ts +89 -0
  164. package/src/three/holdingBlock.ts +926 -0
  165. package/src/three/index.ts +20 -0
  166. package/src/three/itemMesh.ts +427 -0
  167. package/src/three/modules.d.ts +14 -0
  168. package/src/three/panorama.ts +308 -0
  169. package/src/three/panoramaShared.ts +1 -0
  170. package/src/three/renderSlot.ts +82 -0
  171. package/src/three/skyboxRenderer.ts +406 -0
  172. package/src/three/starField.ts +13 -0
  173. package/src/three/threeJsMedia.ts +731 -0
  174. package/src/three/threeJsMethods.ts +15 -0
  175. package/src/three/threeJsParticles.ts +160 -0
  176. package/src/three/threeJsSound.ts +95 -0
  177. package/src/three/threeJsUtils.ts +90 -0
  178. package/src/three/waypointSprite.ts +435 -0
  179. package/src/three/waypoints.ts +163 -0
  180. package/src/three/world/cursorBlock.ts +172 -0
  181. package/src/three/world/vr.ts +257 -0
  182. package/src/three/worldGeometryExport.ts +259 -0
  183. package/src/three/worldGeometryHandler.ts +279 -0
  184. package/src/three/worldRendererThree.ts +1381 -0
  185. package/src/worldView/index.ts +6 -0
  186. package/src/worldView/types.ts +66 -0
  187. 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
+ }