minecraft-renderer 0.1.93 → 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 (42) hide show
  1. package/dist/minecraft-renderer.js +64 -64
  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/types.ts +4 -1
  7. package/src/lib/worldrendererCommon.ts +2 -2
  8. package/src/three/entities.ts +302 -33
  9. package/src/three/entity/EntityMesh.ts +8 -3
  10. package/src/three/entity/animations.js +37 -25
  11. package/src/three/entity/animations.test.ts +249 -0
  12. package/src/three/entity/boatModelRotation.test.ts +29 -0
  13. package/src/three/entity/boatModelRotation.ts +14 -0
  14. package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
  15. package/src/three/entity/boatPaddleAnimation.ts +104 -0
  16. package/src/three/entity/boatPassengerRendering.ts +20 -0
  17. package/src/three/entity/boatPassengerRotation.test.ts +166 -0
  18. package/src/three/entity/boatPassengerRotation.ts +71 -0
  19. package/src/three/entity/boatRenderSetup.test.ts +236 -0
  20. package/src/three/entity/boatRenderSetup.ts +214 -0
  21. package/src/three/entity/entityHeadPose.test.ts +15 -0
  22. package/src/three/entity/entityHeadPose.ts +22 -0
  23. package/src/three/entity/horseEntityScale.test.ts +11 -0
  24. package/src/three/entity/horseEntityScale.ts +9 -0
  25. package/src/three/entity/horseHeadAnimation.test.ts +159 -0
  26. package/src/three/entity/horseHeadAnimation.ts +151 -0
  27. package/src/three/entity/horsePassengerRendering.test.ts +77 -0
  28. package/src/three/entity/interpolationPolicy.test.ts +259 -0
  29. package/src/three/entity/interpolationPolicy.ts +187 -0
  30. package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
  31. package/src/three/entity/localVehicleRendering.integration.test.ts +247 -0
  32. package/src/three/entity/minecartLocalPassengerRendering.test.ts +208 -0
  33. package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
  34. package/src/three/entity/networkHeadPitchRendering.ts +44 -0
  35. package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +227 -0
  36. package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
  37. package/src/three/entity/vehiclePassengerProduction.test.ts +214 -0
  38. package/src/three/entity/vehiclePassengerRendering.ts +56 -0
  39. package/src/three/entity/vehiclePassengerUpdate.ts +82 -0
  40. package/src/three/graphicsBackendBase.ts +5 -5
  41. package/src/three/graphicsBackendOffThread.ts +2 -2
  42. package/src/three/worldRendererThree.ts +48 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.93",
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
  /**
@@ -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,
@@ -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[]> = {}
@@ -450,7 +488,8 @@ export class Entities {
450
488
  const thirdPerson = this.worldRenderer.playerStateUtils.isThirdPerson()
451
489
  entity.visible = thirdPerson
452
490
 
453
- if (thirdPerson) {
491
+ const isAnchoredPassenger = entity.userData._passengerVehicleId != null || entity.userData._boatPassengerVehicleId != null
492
+ if (thirdPerson && !isAnchoredPassenger) {
454
493
  const yOffset = this.worldRenderer.playerStateReactive.eyeHeight
455
494
  entity.position.set(this.worldRenderer.cameraWorldPos.x, this.worldRenderer.cameraWorldPos.y - yOffset, this.worldRenderer.cameraWorldPos.z)
456
495
  }
@@ -462,6 +501,31 @@ export class Entities {
462
501
  playerObject.animation.update(playerObject, dt)
463
502
  }
464
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
+
465
529
  entity.traverse(child => {
466
530
  if (child instanceof Entity.EntityMesh) {
467
531
  child.update(dt)
@@ -482,21 +546,122 @@ export class Entities {
482
546
  this.maybeRenderPlayerSkin(entityIdRaw)
483
547
  }
484
548
 
485
- 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) {
486
555
  this.syncArmorPositions(entity)
487
556
  }
557
+ }
488
558
 
489
- if (isPlayerEntity && entity.visible) {
490
- const rotation = this.worldRenderer.cameraShake.getBaseRotation()
491
- entity.rotation.set(0, rotation.yaw, 0)
559
+ this.updateVehiclePassengerPositions()
560
+ this.applyRemoteBoatPassengerRotations()
561
+ this.applyLocalThirdPersonPlayerRotation()
562
+ this.updateBoatPaddleAnimations(dt)
563
+ }
492
564
 
493
- entity.traverse(c => {
494
- if (c.name === 'head') {
495
- c.rotation.set(-rotation.pitch, 0, 0)
496
- }
497
- })
498
- }
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
+ )
590
+ }
591
+ }
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
499
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()
500
665
  }
501
666
 
502
667
  private syncArmorPositions(entity: SceneEntity) {
@@ -763,7 +928,10 @@ export class Entities {
763
928
  }
764
929
  }
765
930
 
766
- 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 {
767
935
  const anim = playerObject.animation as WalkingGeneralSwing | undefined
768
936
  if (!anim) return
769
937
 
@@ -773,12 +941,21 @@ export class Entities {
773
941
  }
774
942
 
775
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
776
953
  anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
777
954
  anim.isRunning = animation === 'running'
778
955
  anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
779
956
  }
780
957
 
781
- playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
958
+ playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking' | 'riding') {
782
959
  const playerObject =
783
960
  entityPlayerId === 'player_entity' ? this.playerEntity?.playerObject : (this.getPlayerObject(entityPlayerId) ?? this.playerEntity?.playerObject)
784
961
 
@@ -898,6 +1075,10 @@ export class Entities {
898
1075
  if (!e) return
899
1076
  e.userData._posTween?.stop()
900
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)
901
1082
  if (e.additionalCleanup) e.additionalCleanup()
902
1083
  e.traverse(c => {
903
1084
  if (c['additionalCleanup']) c['additionalCleanup']()
@@ -989,6 +1170,11 @@ export class Entities {
989
1170
  }
990
1171
  if (!mesh) return
991
1172
  mesh.name = 'mesh'
1173
+ const boatMeshYawOffset = getBoatMeshYawOffset(entity.name)
1174
+ if (boatMeshYawOffset != null) {
1175
+ mesh.rotation.y = boatMeshYawOffset
1176
+ setupBoatMesh(mesh)
1177
+ }
992
1178
  // set initial position so there are no weird jumps update after
993
1179
  const pos = entity.pos ?? entity.position
994
1180
  this.worldRenderer.sceneOrigin.track(group)
@@ -1016,10 +1202,20 @@ export class Entities {
1016
1202
  this.syncSceneAttachment(group)
1017
1203
 
1018
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
+ }
1019
1210
  } else {
1020
1211
  mesh = e.children.find(c => c.name === 'mesh')
1212
+ if (entity.renderHints) {
1213
+ e.originalEntity.renderHints = entity.renderHints
1214
+ }
1021
1215
  }
1022
1216
 
1217
+ this.applyEntityRenderHints(e, entity)
1218
+
1023
1219
  // Update equipment
1024
1220
  this.updateEntityEquipment(e, entity)
1025
1221
 
@@ -1211,26 +1407,88 @@ export class Entities {
1211
1407
  this.updateEntityPosition(entity, justAdded, overrides)
1212
1408
  }
1213
1409
 
1214
- 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
+ ) {
1215
1452
  const e = this.entities[entity.id]
1216
1453
  if (!e) return
1217
- 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)
1218
1462
  if (entity.position) {
1219
- // Initialize tween target from current world position
1220
- const currentWorld = this.worldRenderer.sceneOrigin.getWorldPosition(e) ?? { x: entity.position.x, y: entity.position.y, z: entity.position.z }
1221
- if (!e.userData._tweenTarget) {
1222
- 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)
1223
1489
  }
1224
- // Stop previous position tween to prevent accumulation
1225
- e.userData._posTween?.stop()
1226
- // Tween a separate target object, apply via proxy on each update
1227
- e.userData._posTween = new TWEEN.Tween(e.userData._tweenTarget)
1228
- .to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION)
1229
- .onUpdate(() => {
1230
- e.position.set(e.userData._tweenTarget.x, e.userData._tweenTarget.y, e.userData._tweenTarget.z)
1231
- })
1232
- .start()
1233
1490
  }
1491
+ const rotationTweenDuration = getEntityRotationTweenDurationMs(entity, justAdded)
1234
1492
  /** World yaw for the whole model: for PlayerObject skins, rotate body to head look dir; head mesh stays yaw-fixed (pitch only). */
1235
1493
  let targetYaw: number | undefined
1236
1494
  if (e.playerObject && overrides?.rotation?.head) {
@@ -1243,18 +1501,29 @@ export class Entities {
1243
1501
  targetYaw = entity.yaw
1244
1502
  }
1245
1503
  if (typeof targetYaw === 'number' && Number.isFinite(targetYaw)) {
1246
- const dy = shortestYawRadians(e.rotation.y, targetYaw)
1247
1504
  // Stop previous rotation tween to prevent accumulation (mirror _posTween)
1248
1505
  e.userData._rotTween?.stop()
1249
- 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
+ }
1250
1514
  }
1251
1515
 
1252
1516
  if (e?.playerObject && overrides?.rotation?.head) {
1253
1517
  const { playerObject } = e
1518
+ const hy = overrides.rotation.head.y
1519
+ storeNetworkHeadYaw(e.userData, hy, entity.yaw)
1254
1520
  playerObject.skin.head.rotation.y = 0
1255
1521
 
1256
1522
  const hp = overrides.rotation.head.x
1257
- 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)
1258
1527
  }
1259
1528
  }
1260
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
@@ -32,6 +32,7 @@ export class WalkingGeneralSwing extends PlayerAnimation {
32
32
  isRunning = false
33
33
  isMoving = true
34
34
  isCrouched = false
35
+ isRiding = false
35
36
 
36
37
  _dt = 0
37
38
  _phase = 0
@@ -127,7 +128,7 @@ export class WalkingGeneralSwing extends PlayerAnimation {
127
128
  if (!this._defaults) this._captureDefaults(player)
128
129
  this._applyDefaults(player)
129
130
 
130
- const targetMove = this.isMoving ? 1 : 0
131
+ const targetMove = this.isRiding ? 0 : this.isMoving ? 1 : 0
131
132
  const kMove = Math.min(1, dt * 20)
132
133
  this._moveBlend += (targetMove - this._moveBlend) * kMove
133
134
 
@@ -139,35 +140,46 @@ export class WalkingGeneralSwing extends PlayerAnimation {
139
140
 
140
141
  applyCrouchPose(player, this.isCrouched ? 1 : 0)
141
142
 
142
- const boundary = this.isRunning ? Math.cos(t) : Math.sin(t)
143
- if (Math.abs(boundary) < 0.02) {
144
- if (this.switchAnimationCallback) {
145
- reset = true
146
- }
147
- }
143
+ if (this.isRiding) {
144
+ player.skin.rightArm.rotation.x -= Math.PI / 5
145
+ player.skin.leftArm.rotation.x -= Math.PI / 5
148
146
 
149
- if (this.isRunning) {
150
- player.skin.leftLeg.rotation.x += Math.cos(t + Math.PI) * 1.3 * this._moveBlend
151
- player.skin.rightLeg.rotation.x += Math.cos(t) * 1.3 * this._moveBlend
147
+ // Vanilla HumanoidModel uses ModelPart Z→Y→X order with yRot/zRot signs as in Java.
148
+ // skinview3d Y/Z axes are opposite the Java model, so ZYX plus inverted Y/Z signs
149
+ // matches vanilla Rz × Ry × Rx (see BOAT_RIDING_LEG_SPREAD.md).
150
+ player.skin.rightLeg.rotation.set(-1.4137167, -Math.PI / 10, -Math.PI / 40, 'ZYX')
151
+ player.skin.leftLeg.rotation.set(-1.4137167, Math.PI / 10, Math.PI / 40, 'ZYX')
152
152
  } else {
153
- player.skin.leftLeg.rotation.x += Math.sin(t) * 0.5 * this._moveBlend
154
- player.skin.rightLeg.rotation.x += Math.sin(t + Math.PI) * 0.5 * this._moveBlend
155
- }
153
+ const boundary = this.isRunning ? Math.cos(t) : Math.sin(t)
154
+ if (Math.abs(boundary) < 0.02) {
155
+ if (this.switchAnimationCallback) {
156
+ reset = true
157
+ }
158
+ }
156
159
 
157
- if (this.isRunning) {
158
- player.skin.leftArm.rotation.x += Math.cos(t) * 1.5 * this._moveBlend
159
- player.skin.rightArm.rotation.x += Math.cos(t + Math.PI) * 1.5 * this._moveBlend
160
+ if (this.isRunning) {
161
+ player.skin.leftLeg.rotation.x += Math.cos(t + Math.PI) * 1.3 * this._moveBlend
162
+ player.skin.rightLeg.rotation.x += Math.cos(t) * 1.3 * this._moveBlend
163
+ } else {
164
+ player.skin.leftLeg.rotation.x += Math.sin(t) * 0.5 * this._moveBlend
165
+ player.skin.rightLeg.rotation.x += Math.sin(t + Math.PI) * 0.5 * this._moveBlend
166
+ }
160
167
 
161
- const basicArmRotationZ = Math.PI * 0.1
162
- player.skin.leftArm.rotation.z += (Math.cos(t) * 0.1 + basicArmRotationZ) * this._moveBlend
163
- player.skin.rightArm.rotation.z += (Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ) * this._moveBlend
164
- } else {
165
- player.skin.leftArm.rotation.x += Math.sin(t + Math.PI) * 0.5 * this._moveBlend
166
- player.skin.rightArm.rotation.x += Math.sin(t) * 0.5 * this._moveBlend
168
+ if (this.isRunning) {
169
+ player.skin.leftArm.rotation.x += Math.cos(t) * 1.5 * this._moveBlend
170
+ player.skin.rightArm.rotation.x += Math.cos(t + Math.PI) * 1.5 * this._moveBlend
167
171
 
168
- const basicArmRotationZ = Math.PI * 0.02
169
- player.skin.leftArm.rotation.z += (Math.cos(t) * 0.03 + basicArmRotationZ) * this._moveBlend
170
- player.skin.rightArm.rotation.z += (Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ) * this._moveBlend
172
+ const basicArmRotationZ = Math.PI * 0.1
173
+ player.skin.leftArm.rotation.z += (Math.cos(t) * 0.1 + basicArmRotationZ) * this._moveBlend
174
+ player.skin.rightArm.rotation.z += (Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ) * this._moveBlend
175
+ } else {
176
+ player.skin.leftArm.rotation.x += Math.sin(t + Math.PI) * 0.5 * this._moveBlend
177
+ player.skin.rightArm.rotation.x += Math.sin(t) * 0.5 * this._moveBlend
178
+
179
+ const basicArmRotationZ = Math.PI * 0.02
180
+ player.skin.leftArm.rotation.z += (Math.cos(t) * 0.03 + basicArmRotationZ) * this._moveBlend
181
+ player.skin.rightArm.rotation.z += (Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ) * this._moveBlend
182
+ }
171
183
  }
172
184
 
173
185
  if (this._swingTime !== null) {