minecraft-renderer 0.1.93 → 0.1.95

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 +303 -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 +281 -0
  29. package/src/three/entity/interpolationPolicy.ts +242 -0
  30. package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
  31. package/src/three/entity/localVehicleRendering.integration.test.ts +267 -0
  32. package/src/three/entity/minecartLocalPassengerRendering.test.ts +210 -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 +229 -0
  36. package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
  37. package/src/three/entity/vehiclePassengerProduction.test.ts +229 -0
  38. package/src/three/entity/vehiclePassengerRendering.ts +56 -0
  39. package/src/three/entity/vehiclePassengerUpdate.ts +91 -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.95",
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,123 @@ 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
+ version: this.worldRenderer.version
660
+ })
661
+ }
662
+
663
+ /** @deprecated Use updateVehiclePassengerPositions */
664
+ updateBoatPassengerPositions() {
665
+ this.updateVehiclePassengerPositions()
500
666
  }
501
667
 
502
668
  private syncArmorPositions(entity: SceneEntity) {
@@ -763,7 +929,10 @@ export class Entities {
763
929
  }
764
930
  }
765
931
 
766
- private applyMovementAnimation(playerObject: PlayerObjectType, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking'): void {
932
+ private applyMovementAnimation(
933
+ playerObject: PlayerObjectType,
934
+ animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking' | 'riding'
935
+ ): void {
767
936
  const anim = playerObject.animation as WalkingGeneralSwing | undefined
768
937
  if (!anim) return
769
938
 
@@ -773,12 +942,21 @@ export class Entities {
773
942
  }
774
943
 
775
944
  anim.switchAnimationCallback = null
945
+ if (animation === 'riding') {
946
+ anim.isMoving = false
947
+ anim.isRunning = false
948
+ anim.isCrouched = false
949
+ anim.isRiding = true
950
+ return
951
+ }
952
+
953
+ anim.isRiding = false
776
954
  anim.isMoving = animation === 'walking' || animation === 'running' || animation === 'crouchWalking'
777
955
  anim.isRunning = animation === 'running'
778
956
  anim.isCrouched = animation === 'crouch' || animation === 'crouchWalking'
779
957
  }
780
958
 
781
- playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking') {
959
+ playAnimation(entityPlayerId, animation: 'walking' | 'running' | 'oneSwing' | 'idle' | 'crouch' | 'crouchWalking' | 'riding') {
782
960
  const playerObject =
783
961
  entityPlayerId === 'player_entity' ? this.playerEntity?.playerObject : (this.getPlayerObject(entityPlayerId) ?? this.playerEntity?.playerObject)
784
962
 
@@ -898,6 +1076,10 @@ export class Entities {
898
1076
  if (!e) return
899
1077
  e.userData._posTween?.stop()
900
1078
  e.userData._rotTween?.stop()
1079
+ e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] = undefined
1080
+ e.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = undefined
1081
+ const boatMesh = e.children.find(c => c.name === 'mesh')
1082
+ if (boatMesh) disposeBoatWaterPatch(boatMesh)
901
1083
  if (e.additionalCleanup) e.additionalCleanup()
902
1084
  e.traverse(c => {
903
1085
  if (c['additionalCleanup']) c['additionalCleanup']()
@@ -989,6 +1171,11 @@ export class Entities {
989
1171
  }
990
1172
  if (!mesh) return
991
1173
  mesh.name = 'mesh'
1174
+ const boatMeshYawOffset = getBoatMeshYawOffset(entity.name)
1175
+ if (boatMeshYawOffset != null) {
1176
+ mesh.rotation.y = boatMeshYawOffset
1177
+ setupBoatMesh(mesh)
1178
+ }
992
1179
  // set initial position so there are no weird jumps update after
993
1180
  const pos = entity.pos ?? entity.position
994
1181
  this.worldRenderer.sceneOrigin.track(group)
@@ -1016,10 +1203,20 @@ export class Entities {
1016
1203
  this.syncSceneAttachment(group)
1017
1204
 
1018
1205
  this.afterAddEntity(entity)
1206
+ const horseHeadRig = mesh.userData[HORSE_HEAD_RIG_USER_DATA_KEY] as HorseHeadRig | undefined
1207
+ if (horseHeadRig) {
1208
+ e.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = horseHeadRig
1209
+ e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] = createHorseHeadAnimationState()
1210
+ }
1019
1211
  } else {
1020
1212
  mesh = e.children.find(c => c.name === 'mesh')
1213
+ if (entity.renderHints) {
1214
+ e.originalEntity.renderHints = entity.renderHints
1215
+ }
1021
1216
  }
1022
1217
 
1218
+ this.applyEntityRenderHints(e, entity)
1219
+
1023
1220
  // Update equipment
1024
1221
  this.updateEntityEquipment(e, entity)
1025
1222
 
@@ -1211,26 +1408,88 @@ export class Entities {
1211
1408
  this.updateEntityPosition(entity, justAdded, overrides)
1212
1409
  }
1213
1410
 
1214
- updateEntityPosition(entity: import('prismarine-entity').Entity, justAdded: boolean, overrides: { rotation?: { head?: { y: number; x: number } } }) {
1411
+ applyEntityRenderHints(e: SceneEntity, entity: SceneEntity['originalEntity']) {
1412
+ if (entity.renderHints) {
1413
+ const hints = entity.renderHints as EntityRenderHints
1414
+ const nextPassengerIds = new Set(hints.passengerIds ?? hints.boatPassengerIds ?? [])
1415
+ const previousPassengerIds = e.userData.renderHints?.passengerIds ?? e.userData.renderHints?.boatPassengerIds ?? []
1416
+ const isPassengerVehicle = isBoatEntityName(entity.name) || isRideableMinecartEntityName(entity.name)
1417
+ if (isPassengerVehicle) {
1418
+ for (const previousPassengerId of previousPassengerIds) {
1419
+ if (nextPassengerIds.has(previousPassengerId)) continue
1420
+ const previousPassenger = this.resolvePassengerEntity(previousPassengerId)
1421
+ if (previousPassenger) {
1422
+ releaseVehiclePassengerPosition(previousPassenger, String(entity.id), this.worldRenderer.sceneOrigin.getWorldPosition(previousPassenger))
1423
+ }
1424
+ }
1425
+ }
1426
+ e.userData.renderHints = entity.renderHints
1427
+ }
1428
+ if (!isBoatEntityName(entity.name)) return
1429
+ const mesh = e.children.find(c => c.name === 'mesh')
1430
+ if (!mesh) return
1431
+ const waterPatch = mesh.userData.boatWaterPatch as THREE.Object3D | undefined
1432
+ if (waterPatch) {
1433
+ waterPatch.visible = entity.renderHints?.boatWaterPatchVisible === true
1434
+ }
1435
+
1436
+ const hints = (e.userData.renderHints ?? entity.renderHints) as EntityRenderHints | undefined
1437
+ const leftActive = hints?.boatPaddleLeft === true
1438
+ const rightActive = hints?.boatPaddleRight === true
1439
+ let animState = mesh.userData.boatPaddleAnimation as BoatPaddleAnimationState | undefined
1440
+ if (!animState) {
1441
+ animState = createBoatPaddleAnimationState()
1442
+ mesh.userData.boatPaddleAnimation = animState
1443
+ }
1444
+ syncBoatPaddleAnimationTargets(animState, leftActive, rightActive)
1445
+ }
1446
+
1447
+ updateEntityPosition(
1448
+ entity: SceneEntity['originalEntity'],
1449
+ justAdded: boolean,
1450
+ overrides: { rotation?: { head?: { y: number; x: number } } },
1451
+ headRotationOnly = false
1452
+ ) {
1215
1453
  const e = this.entities[entity.id]
1216
1454
  if (!e) return
1217
- const ANIMATION_DURATION = justAdded ? 0 : TWEEN_DURATION
1455
+ this.applyEntityRenderHints(e, entity)
1456
+ if (e.userData[HORSE_HEAD_RIG_USER_DATA_KEY]) {
1457
+ e.userData._horseHeadPitch = typeof entity.pitch === 'number' && Number.isFinite(entity.pitch) ? entity.pitch : 0
1458
+ const headYaw = (entity as any).headYaw
1459
+ e.userData._horseHeadYaw = typeof headYaw === 'number' && Number.isFinite(headYaw) ? headYaw : entity.yaw
1460
+ e.userData._horseBodyYaw = typeof entity.yaw === 'number' && Number.isFinite(entity.yaw) ? entity.yaw : 0
1461
+ }
1462
+ const cameraSynced = usesCameraSyncedVehiclePosition(entity)
1218
1463
  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 }
1464
+ if (cameraSynced) {
1465
+ e.userData._localVehicleY = entity.position.y
1466
+ e.userData._posTween?.stop()
1467
+ e.userData._posTween = undefined
1468
+ const worldPos = this.resolveLocalVehicleRenderWorldPos(e)
1469
+ e.position.set(worldPos.x, worldPos.y, worldPos.z)
1470
+ } else {
1471
+ const ANIMATION_DURATION = getEntityTweenDurationMs(entity, justAdded)
1472
+ // Initialize tween target from current world position
1473
+ const currentWorld = this.worldRenderer.sceneOrigin.getWorldPosition(e) ?? { x: entity.position.x, y: entity.position.y, z: entity.position.z }
1474
+ if (!e.userData._tweenTarget) {
1475
+ e.userData._tweenTarget = { x: currentWorld.x, y: currentWorld.y, z: currentWorld.z }
1476
+ }
1477
+ // Stop previous position tween to prevent accumulation
1478
+ e.userData._posTween?.stop()
1479
+ // Tween a separate target object, apply via proxy on each update
1480
+ e.userData._posTween = new TWEEN.Tween(e.userData._tweenTarget)
1481
+ .to({ x: entity.position.x, y: entity.position.y, z: entity.position.z }, ANIMATION_DURATION)
1482
+ .onUpdate(() => {
1483
+ e.position.set(e.userData._tweenTarget.x, e.userData._tweenTarget.y, e.userData._tweenTarget.z)
1484
+ })
1485
+ .start()
1486
+ }
1487
+ const horseAnimation = e.userData[HORSE_HEAD_ANIMATION_USER_DATA_KEY] as HorseHeadAnimationState | undefined
1488
+ if (!headRotationOnly && horseAnimation && Number.isFinite(entity.position.x) && Number.isFinite(entity.position.z)) {
1489
+ setHorseHeadAnimationPosition(horseAnimation, entity.position)
1223
1490
  }
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
1491
  }
1492
+ const rotationTweenDuration = getEntityRotationTweenDurationMs(entity, justAdded)
1234
1493
  /** World yaw for the whole model: for PlayerObject skins, rotate body to head look dir; head mesh stays yaw-fixed (pitch only). */
1235
1494
  let targetYaw: number | undefined
1236
1495
  if (e.playerObject && overrides?.rotation?.head) {
@@ -1243,18 +1502,29 @@ export class Entities {
1243
1502
  targetYaw = entity.yaw
1244
1503
  }
1245
1504
  if (typeof targetYaw === 'number' && Number.isFinite(targetYaw)) {
1246
- const dy = shortestYawRadians(e.rotation.y, targetYaw)
1247
1505
  // Stop previous rotation tween to prevent accumulation (mirror _posTween)
1248
1506
  e.userData._rotTween?.stop()
1249
- e.userData._rotTween = new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, ANIMATION_DURATION).start()
1507
+ e.userData._rotTween = undefined
1508
+ const currentYaw = Number.isFinite(e.rotation.y) ? e.rotation.y : 0
1509
+ const dy = shortestYawRadians(currentYaw, targetYaw)
1510
+ if (rotationTweenDuration === 0) {
1511
+ e.rotation.y = currentYaw + dy
1512
+ } else {
1513
+ e.userData._rotTween = new TWEEN.Tween(e.rotation).to({ y: currentYaw + dy }, rotationTweenDuration).start()
1514
+ }
1250
1515
  }
1251
1516
 
1252
1517
  if (e?.playerObject && overrides?.rotation?.head) {
1253
1518
  const { playerObject } = e
1519
+ const hy = overrides.rotation.head.y
1520
+ storeNetworkHeadYaw(e.userData, hy, entity.yaw)
1254
1521
  playerObject.skin.head.rotation.y = 0
1255
1522
 
1256
1523
  const hp = overrides.rotation.head.x
1257
- playerObject.skin.head.rotation.x = typeof hp === 'number' && Number.isFinite(hp) ? -hp : 0
1524
+ storeNetworkHeadPitch(e.userData, hp)
1525
+ applyNetworkHeadPitch(playerObject, e.userData)
1526
+ } else if (e?.playerObject && typeof entity.yaw === 'number' && Number.isFinite(entity.yaw)) {
1527
+ storeNetworkHeadYaw(e.userData, undefined, entity.yaw)
1258
1528
  }
1259
1529
  }
1260
1530
 
@@ -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) {