minecraft-renderer 0.1.92 → 0.1.94

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 (43) hide show
  1. package/dist/minecraft-renderer.js +68 -68
  2. package/dist/minecraft-renderer.js.meta.json +1 -1
  3. package/dist/threeWorker.js +522 -522
  4. package/package.json +1 -1
  5. package/src/graphicsBackend/appViewer.ts +4 -3
  6. package/src/graphicsBackend/config.ts +1 -1
  7. package/src/graphicsBackend/types.ts +4 -1
  8. package/src/lib/worldrendererCommon.ts +3 -3
  9. package/src/three/entities.ts +329 -43
  10. package/src/three/entity/EntityMesh.ts +8 -3
  11. package/src/three/entity/animations.js +37 -25
  12. package/src/three/entity/animations.test.ts +249 -0
  13. package/src/three/entity/boatModelRotation.test.ts +29 -0
  14. package/src/three/entity/boatModelRotation.ts +14 -0
  15. package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
  16. package/src/three/entity/boatPaddleAnimation.ts +104 -0
  17. package/src/three/entity/boatPassengerRendering.ts +20 -0
  18. package/src/three/entity/boatPassengerRotation.test.ts +166 -0
  19. package/src/three/entity/boatPassengerRotation.ts +71 -0
  20. package/src/three/entity/boatRenderSetup.test.ts +236 -0
  21. package/src/three/entity/boatRenderSetup.ts +214 -0
  22. package/src/three/entity/entityHeadPose.test.ts +15 -0
  23. package/src/three/entity/entityHeadPose.ts +22 -0
  24. package/src/three/entity/horseEntityScale.test.ts +11 -0
  25. package/src/three/entity/horseEntityScale.ts +9 -0
  26. package/src/three/entity/horseHeadAnimation.test.ts +159 -0
  27. package/src/three/entity/horseHeadAnimation.ts +151 -0
  28. package/src/three/entity/horsePassengerRendering.test.ts +77 -0
  29. package/src/three/entity/interpolationPolicy.test.ts +259 -0
  30. package/src/three/entity/interpolationPolicy.ts +187 -0
  31. package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
  32. package/src/three/entity/localVehicleRendering.integration.test.ts +247 -0
  33. package/src/three/entity/minecartLocalPassengerRendering.test.ts +208 -0
  34. package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
  35. package/src/three/entity/networkHeadPitchRendering.ts +44 -0
  36. package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +227 -0
  37. package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
  38. package/src/three/entity/vehiclePassengerProduction.test.ts +214 -0
  39. package/src/three/entity/vehiclePassengerRendering.ts +56 -0
  40. package/src/three/entity/vehiclePassengerUpdate.ts +82 -0
  41. package/src/three/graphicsBackendBase.ts +5 -5
  42. package/src/three/graphicsBackendOffThread.ts +2 -2
  43. package/src/three/worldRendererThree.ts +54 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.92",
3
+ "version": "0.1.94",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,8 @@ import type {
19
19
  GraphicsInitOptions,
20
20
  DisplayWorldOptions,
21
21
  RendererReactiveState,
22
- NonReactiveState
22
+ NonReactiveState,
23
+ UpdateCameraOptions
23
24
  } from './types'
24
25
  import { WorldView, WorldProvider, WorldViewWorker } from '../worldView'
25
26
  import { getInitialPlayerState } from './playerState'
@@ -274,8 +275,8 @@ export class AppViewer {
274
275
  /**
275
276
  * Update camera position and rotation.
276
277
  */
277
- updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void {
278
- this.backend?.updateCamera(pos, yaw, pitch)
278
+ updateCamera(pos: Vec3 | null, yaw: number, pitch: number, options?: UpdateCameraOptions): void {
279
+ this.backend?.updateCamera(pos, yaw, pitch, options)
279
280
  }
280
281
 
281
282
  /**
@@ -22,7 +22,7 @@ export const defaultWorldRendererConfig = {
22
22
  futuristicReveal: false,
23
23
 
24
24
  /** Master toggle for section occlusion graph (smart / cave cull). Disabled in spectator. */
25
- smartCull: true,
25
+ smartCull: false,
26
26
 
27
27
  // Performance settings
28
28
  wasmMesher: true,
@@ -10,6 +10,9 @@ import { ResourcesManagerTransferred } from '../resourcesManager'
10
10
  import { WorldViewWorker } from '../worldView'
11
11
  import { Vec3 } from 'vec3'
12
12
  import { WorldRendererConfig } from './config'
13
+ import type { UpdateCameraOptions } from '../three/entity/interpolationPolicy'
14
+
15
+ export type { CameraMovementMode, UpdateCameraOptions } from '../three/entity/interpolationPolicy'
13
16
 
14
17
  // ============================================================================
15
18
  // Graphics Backend Configuration
@@ -135,7 +138,7 @@ export interface GraphicsBackend {
135
138
  startWorld(options: DisplayWorldOptions): Promise<void>
136
139
  disconnect(): void
137
140
  setRendering(rendering: boolean): void
138
- updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void
141
+ updateCamera(pos: Vec3 | null, yaw: number, pitch: number, options?: UpdateCameraOptions): void
139
142
  soundSystem?: any
140
143
  backendMethods?: any
141
144
  getDebugOverlay?(): { entitiesString?: string; left?: Record<string, string>; right?: Record<string, string> }
@@ -8,7 +8,7 @@ import { subscribeKey } from 'valtio/utils'
8
8
  import { proxy, subscribe } from 'valtio'
9
9
  import type { ResourcesManagerTransferred } from '../resourcesManager/resourcesManager'
10
10
  import { dynamicMcDataFiles } from './buildSharedConfig.mjs'
11
- import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem } from '../graphicsBackend/types'
11
+ import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem, UpdateCameraOptions } from '../graphicsBackend/types'
12
12
  import {
13
13
  HighestBlockInfo,
14
14
  CustomBlockModels,
@@ -57,7 +57,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
57
57
  disableEntities: false,
58
58
  /** Tint section borders by occlusion BFS step (perf debug overlay). */
59
59
  caveCullingDebug: false,
60
- smartCull: true
60
+ smartCull: false
61
61
  // disableParticles: false
62
62
  })
63
63
 
@@ -713,7 +713,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
713
713
 
714
714
  abstract handleWorkerMessage(data: WorkerReceive): void
715
715
 
716
- abstract updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void
716
+ abstract updateCamera(pos: Vec3 | null, yaw: number, pitch: number, options?: UpdateCameraOptions): void
717
717
 
718
718
  abstract render(): void
719
719
 
@@ -20,7 +20,44 @@ import { configurePlayerSkinMaterials, PlayerObjectType } from '../lib/createPla
20
20
  import { getBlockMeshFromModel } from './holdingBlock'
21
21
  import { createItemMesh } from './itemMesh'
22
22
  import * as Entity from './entity/EntityMesh'
23
+ import { setupBoatMesh, disposeBoatWaterPatch } from './entity/boatRenderSetup'
24
+ import { getBoatMeshYawOffset, isBoatEntityName } from './entity/boatModelRotation'
25
+ import { resolveBoatPassengerThirdPersonRotation, shouldApplyBoatPassengerThirdPersonRotation } from './entity/boatPassengerRotation'
26
+ import {
27
+ createBoatPaddleAnimationState,
28
+ createBoatPaddlePivotScratch,
29
+ syncBoatPaddleAnimationTargets,
30
+ updateBoatPaddleAnimationState,
31
+ type BoatPaddleAnimationState
32
+ } from './entity/boatPaddleAnimation'
33
+ import { releaseVehiclePassengerPosition } from './entity/vehiclePassengerRendering'
34
+ import { updateVehiclePassengerPositions as applyVehiclePassengerPositions } from './entity/vehiclePassengerUpdate'
35
+ import { applyNetworkHeadPitch, storeNetworkHeadPitch, storeNetworkHeadYaw } from './entity/networkHeadPitchRendering'
36
+ import { processRemoteBoatPassengerRotations, type RemoteBoatPassengerEntity } from './entity/remoteBoatPassengerRotation'
37
+ import {
38
+ ENTITY_TWEEN_DURATION_MS,
39
+ applyLocalHorseCameraYawLock,
40
+ getEntityTweenDurationMs,
41
+ getEntityRotationTweenDurationMs,
42
+ resolveLocalVehicleWorldPosition,
43
+ shouldApplyLocalHorseCameraYawLock,
44
+ type EntityRenderHints,
45
+ type Vec3Like,
46
+ usesCameraSyncedVehiclePosition,
47
+ isRideableMinecartEntityName
48
+ } from './entity/interpolationPolicy'
23
49
  import { getMesh } from './entity/EntityMesh'
50
+ import {
51
+ HORSE_HEAD_ANIMATION_USER_DATA_KEY,
52
+ HORSE_HEAD_RIG_USER_DATA_KEY,
53
+ applyHorseHeadPose,
54
+ calculateHorseHeadPose,
55
+ createHorseHeadAnimationState,
56
+ setHorseHeadAnimationPosition,
57
+ updateHorseHeadAnimationFrame,
58
+ type HorseHeadAnimationState,
59
+ type HorseHeadRig
60
+ } from './entity/horseHeadAnimation'
24
61
  import { WalkingGeneralSwing } from './entity/animations'
25
62
  import { disposeObject, loadNearestFilterTexture, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
26
63
  import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
@@ -41,7 +78,7 @@ type EntityMetadataVersions = {
41
78
 
42
79
  export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
43
80
 
44
- export const TWEEN_DURATION = 120
81
+ export const TWEEN_DURATION = ENTITY_TWEEN_DURATION_MS
45
82
 
46
83
  const degreesToRadians = (degrees: number) => degrees * (Math.PI / 180)
47
84
 
@@ -265,7 +302,7 @@ export type SceneEntity = THREE.Object3D & {
265
302
  username?: string
266
303
  uuid?: string
267
304
  additionalCleanup?: () => void
268
- originalEntity: import('prismarine-entity').Entity & { delete?; pos?; name; team?: Team }
305
+ originalEntity: import('prismarine-entity').Entity & { delete?; pos?; name; team?: Team; renderHints?: EntityRenderHints }
269
306
  }
270
307
 
271
308
  export class Entities {
@@ -281,6 +318,7 @@ export class Entities {
281
318
  cachedMapsImages = {} as Record<number, string>
282
319
  itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
283
320
  pendingModelOverrides = new Map<string, { parts: EntityModelOverridePart[] }>()
321
+ private boatPaddleScratch = createBoatPaddlePivotScratch()
284
322
 
285
323
  get entitiesByName(): Record<string, SceneEntity[]> {
286
324
  const byName: Record<string, SceneEntity[]> = {}
@@ -316,6 +354,7 @@ export class Entities {
316
354
  }
317
355
 
318
356
  handlePlayerEntity(playerData: SceneEntity['originalEntity']) {
357
+ if (!this.isRenderingAllowed()) return
319
358
  // Create player entity if it doesn't exist
320
359
  if (!this.playerEntity) {
321
360
  // Create the player entity similar to how normal entities are created
@@ -329,7 +368,7 @@ export class Entities {
329
368
 
330
369
  group.name = 'player_entity'
331
370
  this.playerEntity = group
332
- this.worldRenderer.scene.add(group)
371
+ this.syncSceneAttachment(group)
333
372
 
334
373
  void this.updatePlayerSkin(playerData.id, playerData.username, playerData.uuid ?? undefined, stevePngUrl)
335
374
  }
@@ -392,9 +431,27 @@ export class Entities {
392
431
  }
393
432
 
394
433
  setRendering(rendering: boolean, entity: THREE.Object3D | null = null) {
395
- this.currentlyRendering = rendering
396
- for (const ent of entity ? [entity] : Object.values(this.entities)) {
397
- if (rendering) {
434
+ this.currentlyRendering = rendering && this.isRenderingAllowed()
435
+ this.setSceneAttached(this.currentlyRendering, entity)
436
+ }
437
+
438
+ isRenderingAllowed(): boolean {
439
+ return this.worldRenderer.worldRendererConfig.renderEntities !== false && !this.worldRenderer.reactiveDebugParams.disableEntities
440
+ }
441
+
442
+ /** Add or remove entity meshes from the scene (including third-person player). */
443
+ syncSceneAttachment(entity: THREE.Object3D | null = null): void {
444
+ const attached = this.isRenderingAllowed()
445
+ this.currentlyRendering = attached
446
+ this.setSceneAttached(attached, entity)
447
+ }
448
+
449
+ private setSceneAttached(attached: boolean, entity: THREE.Object3D | null = null): void {
450
+ const targets = entity
451
+ ? [entity]
452
+ : [...Object.values(this.entities), ...(this.playerEntity ? [this.playerEntity] : [])]
453
+ for (const ent of targets) {
454
+ if (attached) {
398
455
  if (!this.worldRenderer.scene.children.includes(ent)) this.worldRenderer.scene.add(ent)
399
456
  } else {
400
457
  this.worldRenderer.scene.remove(ent)
@@ -414,10 +471,8 @@ export class Entities {
414
471
  }
415
472
 
416
473
  render() {
417
- const renderEntitiesConfig = this.worldRenderer.worldRendererConfig.renderEntities
418
- if (renderEntitiesConfig !== this.currentlyRendering) {
419
- this.setRendering(renderEntitiesConfig)
420
- }
474
+ this.syncSceneAttachment()
475
+ if (!this.isRenderingAllowed()) return
421
476
 
422
477
  this.clock.update(performance.now())
423
478
  const dt = Math.min(this.clock.getDelta(), 1 / 30)
@@ -433,7 +488,8 @@ export class Entities {
433
488
  const thirdPerson = this.worldRenderer.playerStateUtils.isThirdPerson()
434
489
  entity.visible = thirdPerson
435
490
 
436
- if (thirdPerson) {
491
+ const isAnchoredPassenger = entity.userData._passengerVehicleId != null || entity.userData._boatPassengerVehicleId != null
492
+ if (thirdPerson && !isAnchoredPassenger) {
437
493
  const yOffset = this.worldRenderer.playerStateReactive.eyeHeight
438
494
  entity.position.set(this.worldRenderer.cameraWorldPos.x, this.worldRenderer.cameraWorldPos.y - yOffset, this.worldRenderer.cameraWorldPos.z)
439
495
  }
@@ -445,6 +501,31 @@ export class Entities {
445
501
  playerObject.animation.update(playerObject, dt)
446
502
  }
447
503
 
504
+ if (!isPlayerEntity) {
505
+ const renderHints = entity.userData.renderHints as EntityRenderHints | undefined
506
+ if (shouldApplyLocalHorseCameraYawLock(renderHints)) {
507
+ applyLocalHorseCameraYawLock(entity, this.worldRenderer.cameraShake.getBaseRotation().yaw)
508
+ }
509
+ }
510
+
511
+ const horseHeadRig = entity.userData[HORSE_HEAD_RIG_USER_DATA_KEY] as HorseHeadRig | undefined
512
+ const horseAnimation = entity.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] as HorseHeadAnimationState | undefined
513
+ if (horseHeadRig && horseAnimation) {
514
+ const animation = updateHorseHeadAnimationFrame(horseAnimation, dt)
515
+ const pose = calculateHorseHeadPose({
516
+ entityPitch: entity.userData._horseHeadPitch ?? 0,
517
+ headYaw: entity.userData._horseHeadYaw ?? entity.userData._horseBodyYaw ?? 0,
518
+ bodyYaw: entity.userData._horseBodyYaw ?? 0,
519
+ limbSwing: animation.limbSwing,
520
+ limbSwingAmount: animation.limbSwingAmount
521
+ })
522
+ applyHorseHeadPose(horseHeadRig, pose)
523
+ }
524
+
525
+ if (!isPlayerEntity && playerObject) {
526
+ applyNetworkHeadPitch(playerObject, entity.userData)
527
+ }
528
+
448
529
  entity.traverse(child => {
449
530
  if (child instanceof Entity.EntityMesh) {
450
531
  child.update(dt)
@@ -465,23 +546,124 @@ export class Entities {
465
546
  this.maybeRenderPlayerSkin(entityIdRaw)
466
547
  }
467
548
 
468
- if (entity.visible) {
549
+ if (entity.userData.renderHints?.localVehicle) {
550
+ const worldPos = this.resolveLocalVehicleRenderWorldPos(entity)
551
+ entity.position.set(worldPos.x, worldPos.y, worldPos.z)
552
+ }
553
+
554
+ if (entity.visible && !isPlayerEntity) {
469
555
  this.syncArmorPositions(entity)
470
556
  }
557
+ }
471
558
 
472
- if (isPlayerEntity && entity.visible) {
473
- const rotation = this.worldRenderer.cameraShake.getBaseRotation()
474
- entity.rotation.set(0, rotation.yaw, 0)
559
+ this.updateVehiclePassengerPositions()
560
+ this.applyRemoteBoatPassengerRotations()
561
+ this.applyLocalThirdPersonPlayerRotation()
562
+ this.updateBoatPaddleAnimations(dt)
563
+ }
475
564
 
476
- entity.traverse(c => {
477
- if (c.name === 'head') {
478
- c.rotation.set(-rotation.pitch, 0, 0)
479
- }
480
- })
481
- }
565
+ private applyRemoteBoatPassengerRotations() {
566
+ processRemoteBoatPassengerRotations({
567
+ entities: this.entities as Record<string, RemoteBoatPassengerEntity>,
568
+ syncArmor: entity => this.syncArmorPositions(entity as SceneEntity)
569
+ })
570
+ }
571
+
572
+ private updateBoatPaddleAnimations(dt: number) {
573
+ for (const entity of Object.values(this.entities)) {
574
+ if (!entity) continue
575
+ if (!isBoatEntityName(entity['realName'] as string | undefined)) continue
576
+
577
+ const mesh = entity.children.find(child => child.name === 'mesh')
578
+ if (!mesh) continue
579
+
580
+ let animState = mesh.userData.boatPaddleAnimation as BoatPaddleAnimationState | undefined
581
+ if (!animState) continue
582
+
583
+ updateBoatPaddleAnimationState(
584
+ animState,
585
+ dt,
586
+ mesh.userData.boatPaddleLeftPivot as THREE.Object3D | undefined,
587
+ mesh.userData.boatPaddleRightPivot as THREE.Object3D | undefined,
588
+ this.boatPaddleScratch
589
+ )
482
590
  }
483
591
  }
484
592
 
593
+ private applyLocalThirdPersonPlayerRotation() {
594
+ const entity = this.playerEntity
595
+ if (!entity?.visible || !entity.playerObject) return
596
+
597
+ const rotation = this.worldRenderer.cameraShake.getBaseRotation()
598
+ const isAnchoredPassenger = entity.userData._passengerVehicleId != null || entity.userData._boatPassengerVehicleId != null
599
+ const anchoredVehicleId = entity.userData._passengerVehicleId ?? entity.userData._boatPassengerVehicleId
600
+ const vehicle = anchoredVehicleId != null ? this.entities[anchoredVehicleId] : undefined
601
+ const vehicleName = vehicle?.['realName'] ?? vehicle?.originalEntity.name
602
+ const vehicleYaw = vehicle?.rotation.y
603
+
604
+ if (
605
+ shouldApplyBoatPassengerThirdPersonRotation({
606
+ isThirdPerson: true,
607
+ isAnchoredPassenger,
608
+ vehicleName,
609
+ vehicleYaw
610
+ })
611
+ ) {
612
+ const resolved = resolveBoatPassengerThirdPersonRotation({
613
+ cameraYaw: rotation.yaw,
614
+ cameraPitch: rotation.pitch,
615
+ vehicleYaw: vehicleYaw!
616
+ })
617
+ entity.rotation.set(0, resolved.bodyYaw, 0)
618
+ entity.traverse(child => {
619
+ if (child.name !== 'head') return
620
+ child.rotation.set(resolved.headPitch, resolved.headYaw, 0)
621
+ })
622
+ } else {
623
+ entity.rotation.set(0, rotation.yaw, 0)
624
+ entity.traverse(child => {
625
+ if (child.name !== 'head') return
626
+ child.rotation.set(-rotation.pitch, 0, 0)
627
+ })
628
+ }
629
+
630
+ this.syncArmorPositions(entity)
631
+ }
632
+
633
+ private resolvePassengerEntity(passengerId: number): SceneEntity | undefined {
634
+ if (this.playerEntity?.originalEntity.id === passengerId) {
635
+ return this.playerEntity
636
+ }
637
+ return this.entities[passengerId]
638
+ }
639
+
640
+ private resolveLocalVehicleRenderWorldPos(sceneEntity: SceneEntity): Vec3Like {
641
+ const originalEntity = sceneEntity.originalEntity
642
+ const renderHints = sceneEntity.userData.renderHints as EntityRenderHints | undefined
643
+ const rawVehicleY = sceneEntity.userData._localVehicleY ?? originalEntity.position?.y ?? sceneEntity.position.y
644
+ return resolveLocalVehicleWorldPosition({
645
+ cameraWorldPos: this.worldRenderer.cameraWorldPos,
646
+ rawVehicleY,
647
+ eyeHeight: this.worldRenderer.playerStateReactive.eyeHeight,
648
+ vehicleName: sceneEntity['realName'] ?? originalEntity.name,
649
+ vehicleHeight: originalEntity.height ?? 1.6,
650
+ verticalCameraLock: renderHints?.localVehicleVerticalCameraLock
651
+ })
652
+ }
653
+
654
+ updateVehiclePassengerPositions() {
655
+ applyVehiclePassengerPositions({
656
+ entities: this.entities,
657
+ localPlayer: this.playerEntity,
658
+ getWorldPosition: target => this.worldRenderer.sceneOrigin.getWorldPosition(target as SceneEntity)
659
+ })
660
+ }
661
+
662
+ /** @deprecated Use updateVehiclePassengerPositions */
663
+ updateBoatPassengerPositions() {
664
+ this.updateVehiclePassengerPositions()
665
+ }
666
+
485
667
  private syncArmorPositions(entity: SceneEntity) {
486
668
  if (!entity.playerObject) return
487
669
 
@@ -746,7 +928,10 @@ export class Entities {
746
928
  }
747
929
  }
748
930
 
749
- private applyMovementAnimation(playerObject: PlayerObjectType, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking'): void {
931
+ private applyMovementAnimation(
932
+ playerObject: PlayerObjectType,
933
+ animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking' | 'riding'
934
+ ): void {
750
935
  const anim = playerObject.animation as WalkingGeneralSwing | undefined
751
936
  if (!anim) return
752
937
 
@@ -756,12 +941,21 @@ export class Entities {
756
941
  }
757
942
 
758
943
  anim.switchAnimationCallback = null
944
+ if (animation === 'riding') {
945
+ anim.isMoving = false
946
+ anim.isRunning = false
947
+ anim.isCrouched = false
948
+ anim.isRiding = true
949
+ return
950
+ }
951
+
952
+ anim.isRiding = false
759
953
  anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
760
954
  anim.isRunning = animation === 'running'
761
955
  anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
762
956
  }
763
957
 
764
- playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
958
+ playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking' | 'riding') {
765
959
  const playerObject =
766
960
  entityPlayerId === 'player_entity' ? this.playerEntity?.playerObject : (this.getPlayerObject(entityPlayerId) ?? this.playerEntity?.playerObject)
767
961
 
@@ -881,6 +1075,10 @@ export class Entities {
881
1075
  if (!e) return
882
1076
  e.userData._posTween?.stop()
883
1077
  e.userData._rotTween?.stop()
1078
+ e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] = undefined
1079
+ e.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = undefined
1080
+ const boatMesh = e.children.find(c => c.name === 'mesh')
1081
+ if (boatMesh) disposeBoatWaterPatch(boatMesh)
884
1082
  if (e.additionalCleanup) e.additionalCleanup()
885
1083
  e.traverse(c => {
886
1084
  if (c['additionalCleanup']) c['additionalCleanup']()
@@ -892,6 +1090,7 @@ export class Entities {
892
1090
  delete this.entities[entity.id]
893
1091
  return
894
1092
  }
1093
+ if (!this.isRenderingAllowed()) return
895
1094
 
896
1095
  let mesh: THREE.Object3D | undefined
897
1096
  if (e === undefined) {
@@ -971,6 +1170,11 @@ export class Entities {
971
1170
  }
972
1171
  if (!mesh) return
973
1172
  mesh.name = 'mesh'
1173
+ const boatMeshYawOffset = getBoatMeshYawOffset(entity.name)
1174
+ if (boatMeshYawOffset != null) {
1175
+ mesh.rotation.y = boatMeshYawOffset
1176
+ setupBoatMesh(mesh)
1177
+ }
974
1178
  // set initial position so there are no weird jumps update after
975
1179
  const pos = entity.pos ?? entity.position
976
1180
  this.worldRenderer.sceneOrigin.track(group)
@@ -985,7 +1189,6 @@ export class Entities {
985
1189
  group.add(mesh)
986
1190
  group.add(boxHelper)
987
1191
  boxHelper.visible = false
988
- this.worldRenderer.scene.add(group)
989
1192
 
990
1193
  e = group
991
1194
  e.name = 'entity'
@@ -996,13 +1199,23 @@ export class Entities {
996
1199
  void this.updatePlayerSkin(entity.id, entity.username, overrides?.texture ? entity.uuid : undefined, overrides?.texture || stevePngUrl)
997
1200
  }
998
1201
  this.setDebugMode(this.debugMode, group)
999
- this.setRendering(this.currentlyRendering, group)
1202
+ this.syncSceneAttachment(group)
1000
1203
 
1001
1204
  this.afterAddEntity(entity)
1205
+ const horseHeadRig = mesh.userData[HORSE_HEAD_RIG_USER_DATA_KEY] as HorseHeadRig | undefined
1206
+ if (horseHeadRig) {
1207
+ e.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = horseHeadRig
1208
+ e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] = createHorseHeadAnimationState()
1209
+ }
1002
1210
  } else {
1003
1211
  mesh = e.children.find(c => c.name === 'mesh')
1212
+ if (entity.renderHints) {
1213
+ e.originalEntity.renderHints = entity.renderHints
1214
+ }
1004
1215
  }
1005
1216
 
1217
+ this.applyEntityRenderHints(e, entity)
1218
+
1006
1219
  // Update equipment
1007
1220
  this.updateEntityEquipment(e, entity)
1008
1221
 
@@ -1194,26 +1407,88 @@ export class Entities {
1194
1407
  this.updateEntityPosition(entity, justAdded, overrides)
1195
1408
  }
1196
1409
 
1197
- updateEntityPosition(entity: import('prismarine-entity').Entity, justAdded: boolean, overrides: { rotation?: { head?: { y: number; x: number } } }) {
1410
+ applyEntityRenderHints(e: SceneEntity, entity: SceneEntity['originalEntity']) {
1411
+ if (entity.renderHints) {
1412
+ const hints = entity.renderHints as EntityRenderHints
1413
+ const nextPassengerIds = new Set(hints.passengerIds ?? hints.boatPassengerIds ?? [])
1414
+ const previousPassengerIds = e.userData.renderHints?.passengerIds ?? e.userData.renderHints?.boatPassengerIds ?? []
1415
+ const isPassengerVehicle = isBoatEntityName(entity.name) || isRideableMinecartEntityName(entity.name)
1416
+ if (isPassengerVehicle) {
1417
+ for (const previousPassengerId of previousPassengerIds) {
1418
+ if (nextPassengerIds.has(previousPassengerId)) continue
1419
+ const previousPassenger = this.resolvePassengerEntity(previousPassengerId)
1420
+ if (previousPassenger) {
1421
+ releaseVehiclePassengerPosition(previousPassenger, String(entity.id), this.worldRenderer.sceneOrigin.getWorldPosition(previousPassenger))
1422
+ }
1423
+ }
1424
+ }
1425
+ e.userData.renderHints = entity.renderHints
1426
+ }
1427
+ if (!isBoatEntityName(entity.name)) return
1428
+ const mesh = e.children.find(c => c.name === 'mesh')
1429
+ if (!mesh) return
1430
+ const waterPatch = mesh.userData.boatWaterPatch as THREE.Object3D | undefined
1431
+ if (waterPatch) {
1432
+ waterPatch.visible = entity.renderHints?.boatWaterPatchVisible === true
1433
+ }
1434
+
1435
+ const hints = (e.userData.renderHints ?? entity.renderHints) as EntityRenderHints | undefined
1436
+ const leftActive = hints?.boatPaddleLeft === true
1437
+ const rightActive = hints?.boatPaddleRight === true
1438
+ let animState = mesh.userData.boatPaddleAnimation as BoatPaddleAnimationState | undefined
1439
+ if (!animState) {
1440
+ animState = createBoatPaddleAnimationState()
1441
+ mesh.userData.boatPaddleAnimation = animState
1442
+ }
1443
+ syncBoatPaddleAnimationTargets(animState, leftActive, rightActive)
1444
+ }
1445
+
1446
+ updateEntityPosition(
1447
+ entity: SceneEntity['originalEntity'],
1448
+ justAdded: boolean,
1449
+ overrides: { rotation?: { head?: { y: number; x: number } } },
1450
+ headRotationOnly = false
1451
+ ) {
1198
1452
  const e = this.entities[entity.id]
1199
1453
  if (!e) return
1200
- const ANIMATION_DURATION = justAdded ? 0 : TWEEN_DURATION
1454
+ this.applyEntityRenderHints(e, entity)
1455
+ if (e.userData[HORSE_HEAD_RIG_USER_DATA_KEY]) {
1456
+ e.userData._horseHeadPitch = typeof entity.pitch === 'number' && Number.isFinite(entity.pitch) ? entity.pitch : 0
1457
+ const headYaw = (entity as any).headYaw
1458
+ e.userData._horseHeadYaw = typeof headYaw === 'number' && Number.isFinite(headYaw) ? headYaw : entity.yaw
1459
+ e.userData._horseBodyYaw = typeof entity.yaw === 'number' && Number.isFinite(entity.yaw) ? entity.yaw : 0
1460
+ }
1461
+ const cameraSynced = usesCameraSyncedVehiclePosition(entity)
1201
1462
  if (entity.position) {
1202
- // Initialize tween target from current world position
1203
- const currentWorld = this.worldRenderer.sceneOrigin.getWorldPosition(e) ?? { x: entity.position.x, y: entity.position.y, z: entity.position.z }
1204
- if (!e.userData._tweenTarget) {
1205
- e.userData._tweenTarget = { x: currentWorld.x, y: currentWorld.y, z: currentWorld.z }
1463
+ if (cameraSynced) {
1464
+ e.userData._localVehicleY = entity.position.y
1465
+ e.userData._posTween?.stop()
1466
+ e.userData._posTween = undefined
1467
+ const worldPos = this.resolveLocalVehicleRenderWorldPos(e)
1468
+ e.position.set(worldPos.x, worldPos.y, worldPos.z)
1469
+ } else {
1470
+ const ANIMATION_DURATION = getEntityTweenDurationMs(entity, justAdded)
1471
+ // Initialize tween target from current world position
1472
+ const currentWorld = this.worldRenderer.sceneOrigin.getWorldPosition(e) ?? { x: entity.position.x, y: entity.position.y, z: entity.position.z }
1473
+ if (!e.userData._tweenTarget) {
1474
+ e.userData._tweenTarget = { x: currentWorld.x, y: currentWorld.y, z: currentWorld.z }
1475
+ }
1476
+ // Stop previous position tween to prevent accumulation
1477
+ e.userData._posTween?.stop()
1478
+ // Tween a separate target object, apply via proxy on each update
1479
+ e.userData._posTween = new TWEEN.Tween(e.userData._tweenTarget)
1480
+ .to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION)
1481
+ .onUpdate(() => {
1482
+ e.position.set(e.userData._tweenTarget.x, e.userData._tweenTarget.y, e.userData._tweenTarget.z)
1483
+ })
1484
+ .start()
1485
+ }
1486
+ const horseAnimation = e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] as HorseHeadAnimationState | undefined
1487
+ if (!headRotationOnly && horseAnimation && Number.isFinite(entity.position.x) && Number.isFinite(entity.position.z)) {
1488
+ setHorseHeadAnimationPosition(horseAnimation, entity.position)
1206
1489
  }
1207
- // Stop previous position tween to prevent accumulation
1208
- e.userData._posTween?.stop()
1209
- // Tween a separate target object, apply via proxy on each update
1210
- e.userData._posTween = new TWEEN.Tween(e.userData._tweenTarget)
1211
- .to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION)
1212
- .onUpdate(() => {
1213
- e.position.set(e.userData._tweenTarget.x, e.userData._tweenTarget.y, e.userData._tweenTarget.z)
1214
- })
1215
- .start()
1216
1490
  }
1491
+ const rotationTweenDuration = getEntityRotationTweenDurationMs(entity, justAdded)
1217
1492
  /** World yaw for the whole model: for PlayerObject skins, rotate body to head look dir; head mesh stays yaw-fixed (pitch only). */
1218
1493
  let targetYaw: number | undefined
1219
1494
  if (e.playerObject && overrides?.rotation?.head) {
@@ -1226,18 +1501,29 @@ export class Entities {
1226
1501
  targetYaw = entity.yaw
1227
1502
  }
1228
1503
  if (typeof targetYaw === 'number' && Number.isFinite(targetYaw)) {
1229
- const dy = shortestYawRadians(e.rotation.y, targetYaw)
1230
1504
  // Stop previous rotation tween to prevent accumulation (mirror _posTween)
1231
1505
  e.userData._rotTween?.stop()
1232
- e.userData._rotTween = new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, ANIMATION_DURATION).start()
1506
+ e.userData._rotTween = undefined
1507
+ const currentYaw = Number.isFinite(e.rotation.y) ? e.rotation.y : 0
1508
+ const dy = shortestYawRadians(currentYaw, targetYaw)
1509
+ if (rotationTweenDuration === 0) {
1510
+ e.rotation.y = currentYaw + dy
1511
+ } else {
1512
+ e.userData._rotTween = new TWEEN.Tween(e.rotation).to({ y: currentYaw + dy }, rotationTweenDuration).start()
1513
+ }
1233
1514
  }
1234
1515
 
1235
1516
  if (e?.playerObject && overrides?.rotation?.head) {
1236
1517
  const { playerObject } = e
1518
+ const hy = overrides.rotation.head.y
1519
+ storeNetworkHeadYaw(e.userData, hy, entity.yaw)
1237
1520
  playerObject.skin.head.rotation.y = 0
1238
1521
 
1239
1522
  const hp = overrides.rotation.head.x
1240
- playerObject.skin.head.rotation.x = typeof hp === 'number' && Number.isFinite(hp) ? -hp : 0
1523
+ storeNetworkHeadPitch(e.userData, hp)
1524
+ applyNetworkHeadPitch(playerObject, e.userData)
1525
+ } else if (e?.playerObject && typeof entity.yaw === 'number' && Number.isFinite(entity.yaw)) {
1526
+ storeNetworkHeadYaw(e.userData, undefined, entity.yaw)
1241
1527
  }
1242
1528
  }
1243
1529
 
@@ -14,6 +14,9 @@ import entities from './entities.json'
14
14
  import { externalModels } from './objModels'
15
15
  import externalTexturesJson from './externalTextures.json'
16
16
  import { createAnimatedObject } from './gltfAnimationUtils'
17
+ import { BOAT_OBJ_OFFSET_Y } from './boatRenderSetup'
18
+ import { VANILLA_117_HORSE_ENTITY_SCALES } from './horseEntityScale'
19
+ import { createHorseHeadRig } from './horseHeadAnimation'
17
20
 
18
21
  interface ElemFace {
19
22
  dir: [number, number, number]
@@ -441,6 +444,7 @@ const getEntity = (name: string) => {
441
444
  }
442
445
 
443
446
  const scaleEntity: Record<string, number> = {
447
+ ...VANILLA_117_HORSE_ENTITY_SCALES,
444
448
  zombie: 1.85,
445
449
  husk: 1.85,
446
450
  arrow: 0.0025
@@ -449,7 +453,7 @@ const scaleEntity: Record<string, number> = {
449
453
  const offsetEntity: Record<string, Vec3> = {
450
454
  zombie: new Vec3(0, 1, 0),
451
455
  husk: new Vec3(0, 1, 0),
452
- boat: new Vec3(0, -1, 0),
456
+ boat: new Vec3(0, BOAT_OBJ_OFFSET_Y, 0),
453
457
  arrow: new Vec3(0, -0.9, 0)
454
458
  }
455
459
 
@@ -627,14 +631,14 @@ export class EntityMesh {
627
631
  const obj = objLoader.parse(externalModels[type])
628
632
  const scale = scaleEntity[originalType] || scaleEntity[type]
629
633
  if (scale) obj.scale.set(scale, scale, scale)
630
- const offset = offsetEntity[originalType]
634
+ const offset = offsetEntity[originalType] ?? offsetEntity[type]
631
635
  if (offset) obj.position.set(offset.x, offset.y, offset.z)
632
636
  obj.traverse(child => {
633
637
  if (child instanceof THREE.Mesh) {
634
638
  child.material = material
635
639
  // todo
636
640
  if (child.name === 'Head layer') child.visible = false
637
- if (child.name === 'Head' && overrides.rotation?.head) {
641
+ if (type !== 'horse' && child.name === 'Head' && overrides.rotation?.head) {
638
642
  // todo
639
643
  child.rotation.x -= ((overrides.rotation.head.x ?? 0) * Math.PI) / 180
640
644
  child.rotation.y -= ((overrides.rotation.head.y ?? 0) * Math.PI) / 180
@@ -642,6 +646,7 @@ export class EntityMesh {
642
646
  }
643
647
  }
644
648
  })
649
+ if (type === 'horse') createHorseHeadRig(obj)
645
650
  this.mesh = obj
646
651
  debugFlags.type = 'obj'
647
652
  return