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
@@ -0,0 +1,227 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import { PlayerObject } from 'skinview3d'
4
+ import { expect, test } from 'vitest'
5
+ import type { PlayerObjectType } from '../../lib/createPlayerObject'
6
+ import { BOAT_PASSENGER_MAX_HEAD_YAW_RAD, clampBoatPassengerRelativeHeadYaw, normalizeYawDelta } from './boatPassengerRotation'
7
+ import { storeNetworkHeadPitch, storeNetworkHeadYaw } from './networkHeadPitchRendering'
8
+ import {
9
+ applyRemoteBoatPassengerRotation,
10
+ processRemoteBoatPassengerRotation,
11
+ processRemoteBoatPassengerRotations,
12
+ restoreGenericRemotePassengerRotation,
13
+ type RemoteBoatPassengerEntity,
14
+ type RemoteBoatPassengerVehicle
15
+ } from './remoteBoatPassengerRotation'
16
+ import { anchorVehiclePassengerPosition } from './vehiclePassengerRendering'
17
+ import { updateVehiclePassengerPositions } from './vehiclePassengerUpdate'
18
+ import { SceneOrigin } from '../sceneOrigin'
19
+
20
+ const BOAT_YAW = -0.5645049299419149
21
+ const REMOTE_PLAYER_A = 8
22
+ const REMOTE_PLAYER_B = 9
23
+ const BOAT_ID = 10
24
+
25
+ function makePlayerObject(): PlayerObjectType {
26
+ return new PlayerObject() as PlayerObjectType
27
+ }
28
+
29
+ function makeRemotePassenger(id: number, lookYaw = BOAT_YAW) {
30
+ const passenger = new THREE.Group() as RemoteBoatPassengerEntity & THREE.Group & { originalEntity: { id: number } }
31
+ passenger.playerObject = makePlayerObject()
32
+ passenger.originalEntity = { id }
33
+ passenger.userData = {}
34
+ passenger.visible = true
35
+ passenger.rotation.y = lookYaw
36
+ storeNetworkHeadYaw(passenger.userData, lookYaw)
37
+ return passenger
38
+ }
39
+
40
+ function makeBoat(passengerIds: number[], yaw = BOAT_YAW) {
41
+ const boat = new THREE.Group() as RemoteBoatPassengerVehicle & THREE.Group & { originalEntity: { id: number; name: string; height: number } }
42
+ boat.originalEntity = { id: BOAT_ID, name: 'oak_boat', height: 1.6 }
43
+ boat.userData = {
44
+ renderHints: {
45
+ passengerLayout: 'boat',
46
+ passengerIds
47
+ }
48
+ }
49
+ boat.realName = 'oak_boat'
50
+ boat.rotation.y = yaw
51
+ return boat
52
+ }
53
+
54
+ function createHarness(passengerIds: number[]) {
55
+ const sceneOrigin = new SceneOrigin(new THREE.Scene())
56
+ sceneOrigin.update(96, 60, 192)
57
+
58
+ const entities: Record<string, RemoteBoatPassengerEntity & THREE.Group & { originalEntity: { id: number } }> = {}
59
+ for (const id of passengerIds) {
60
+ const passenger = makeRemotePassenger(id)
61
+ sceneOrigin.track(passenger)
62
+ entities[String(id)] = passenger
63
+ }
64
+
65
+ const boat = makeBoat(passengerIds)
66
+ sceneOrigin.track(boat)
67
+ boat.position.set(100, 64, 200)
68
+ entities[String(BOAT_ID)] = boat
69
+
70
+ const runPassengerUpdate = () =>
71
+ updateVehiclePassengerPositions({
72
+ entities: entities as Parameters<typeof updateVehiclePassengerPositions>[0]['entities'],
73
+ localPlayer: null,
74
+ getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D)
75
+ })
76
+
77
+ const runRemoteRotation = () =>
78
+ processRemoteBoatPassengerRotations({
79
+ entities,
80
+ syncArmor: () => undefined
81
+ })
82
+
83
+ return { sceneOrigin, entities, boat, runPassengerUpdate, runRemoteRotation }
84
+ }
85
+
86
+ test('remote anchored boat passenger uses boat yaw for body and relative head yaw', () => {
87
+ const lookYaw = BOAT_YAW + 0.6
88
+ const { entities, boat, runPassengerUpdate, runRemoteRotation } = createHarness([REMOTE_PLAYER_A])
89
+ const passenger = entities[String(REMOTE_PLAYER_A)]!
90
+ storeNetworkHeadYaw(passenger.userData, lookYaw)
91
+ passenger.rotation.y = lookYaw
92
+
93
+ runPassengerUpdate()
94
+ runRemoteRotation()
95
+
96
+ expect(passenger.userData._remoteBoatRotationApplied).toBe(true)
97
+ expect(passenger.rotation.y).toBeCloseTo(BOAT_YAW)
98
+ expect(passenger.playerObject!.skin.head.rotation.y).toBeCloseTo(clampBoatPassengerRelativeHeadYaw(normalizeYawDelta(BOAT_YAW, lookYaw)))
99
+ expect(boat.rotation.y).toBeCloseTo(BOAT_YAW)
100
+ })
101
+
102
+ test('remote boat passenger head yaw updates when boat turns without new look packet', () => {
103
+ const lookYaw = BOAT_YAW + 0.4
104
+ const { entities, boat, runPassengerUpdate, runRemoteRotation } = createHarness([REMOTE_PLAYER_A])
105
+ const passenger = entities[String(REMOTE_PLAYER_A)]!
106
+ storeNetworkHeadYaw(passenger.userData, lookYaw)
107
+
108
+ runPassengerUpdate()
109
+ runRemoteRotation()
110
+ expect(passenger.rotation.y).toBeCloseTo(BOAT_YAW)
111
+
112
+ boat.rotation.y = BOAT_YAW + 0.7
113
+ runPassengerUpdate()
114
+ runRemoteRotation()
115
+
116
+ expect(passenger.rotation.y).toBeCloseTo(BOAT_YAW + 0.7)
117
+ expect(passenger.playerObject!.skin.head.rotation.y).toBeCloseTo(clampBoatPassengerRelativeHeadYaw(normalizeYawDelta(BOAT_YAW + 0.7, lookYaw)))
118
+ })
119
+
120
+ test('two remote passengers keep independent head yaw on the same boat', () => {
121
+ const lookA = BOAT_YAW + 0.3
122
+ const lookB = BOAT_YAW - 0.5
123
+ const { entities, runPassengerUpdate, runRemoteRotation } = createHarness([REMOTE_PLAYER_A, REMOTE_PLAYER_B])
124
+ const passengerA = entities[String(REMOTE_PLAYER_A)]!
125
+ const passengerB = entities[String(REMOTE_PLAYER_B)]!
126
+
127
+ storeNetworkHeadYaw(passengerA.userData, lookA)
128
+ storeNetworkHeadYaw(passengerB.userData, lookB)
129
+
130
+ runPassengerUpdate()
131
+ runRemoteRotation()
132
+
133
+ expect(passengerA.playerObject!.skin.head.rotation.y).toBeCloseTo(clampBoatPassengerRelativeHeadYaw(normalizeYawDelta(BOAT_YAW, lookA)))
134
+ expect(passengerB.playerObject!.skin.head.rotation.y).toBeCloseTo(clampBoatPassengerRelativeHeadYaw(normalizeYawDelta(BOAT_YAW, lookB)))
135
+ })
136
+
137
+ test('dismount restores generic remote body yaw and zero head yaw', () => {
138
+ const lookYaw = BOAT_YAW + 0.55
139
+ const { entities, boat, runPassengerUpdate, runRemoteRotation } = createHarness([REMOTE_PLAYER_A])
140
+ const passenger = entities[String(REMOTE_PLAYER_A)]!
141
+ storeNetworkHeadYaw(passenger.userData, lookYaw)
142
+ storeNetworkHeadPitch(passenger.userData, 0.25)
143
+
144
+ runPassengerUpdate()
145
+ runRemoteRotation()
146
+ expect(passenger.rotation.y).toBeCloseTo(BOAT_YAW)
147
+ expect(passenger.userData._remoteBoatRotationApplied).toBe(true)
148
+
149
+ boat.userData.renderHints = { passengerLayout: 'boat', passengerIds: [] }
150
+ runPassengerUpdate()
151
+ runRemoteRotation()
152
+
153
+ expect(passenger.userData._remoteBoatRotationApplied).toBe(false)
154
+ expect(passenger.rotation.y).toBeCloseTo(lookYaw)
155
+ expect(passenger.playerObject!.skin.head.rotation.y).toBe(0)
156
+ expect(passenger.playerObject!.skin.head.rotation.x).toBeCloseTo(-0.25)
157
+ })
158
+
159
+ test('missing network yaw on first frame keeps generic pose until look arrives', () => {
160
+ const passenger = makeRemotePassenger(REMOTE_PLAYER_A, BOAT_YAW + 0.9)
161
+ delete passenger.userData._networkHeadYaw
162
+ const boat = makeBoat([REMOTE_PLAYER_A])
163
+ anchorVehiclePassengerPosition(passenger, { x: 0, y: 0, z: 0 }, String(BOAT_ID))
164
+
165
+ processRemoteBoatPassengerRotation({
166
+ passenger,
167
+ vehicle: boat,
168
+ syncArmor: () => undefined
169
+ })
170
+
171
+ expect(passenger.userData._remoteBoatRotationApplied).toBeUndefined()
172
+ expect(passenger.rotation.y).toBeCloseTo(BOAT_YAW + 0.9)
173
+ expect(passenger.playerObject!.skin.head.rotation.y).toBe(0)
174
+ })
175
+
176
+ test('restoreGenericRemotePassengerRotation uses stored network yaw', () => {
177
+ const lookYaw = BOAT_YAW + 1.1
178
+ const passenger = makeRemotePassenger(REMOTE_PLAYER_A, BOAT_YAW)
179
+ storeNetworkHeadYaw(passenger.userData, lookYaw)
180
+ storeNetworkHeadPitch(passenger.userData, -0.15)
181
+ passenger.userData._remoteBoatRotationApplied = true
182
+
183
+ restoreGenericRemotePassengerRotation(passenger)
184
+
185
+ expect(passenger.rotation.y).toBeCloseTo(lookYaw)
186
+ expect(passenger.playerObject!.skin.head.rotation.y).toBe(0)
187
+ expect(passenger.playerObject!.skin.head.rotation.x).toBeCloseTo(0.15)
188
+ expect(passenger.userData._remoteBoatRotationApplied).toBe(false)
189
+ })
190
+
191
+ test('applyRemoteBoatPassengerRotation stops active rotation tween', () => {
192
+ const lookYaw = BOAT_YAW + 0.2
193
+ const passenger = makeRemotePassenger(REMOTE_PLAYER_A, lookYaw)
194
+ const boat = makeBoat([REMOTE_PLAYER_A])
195
+ let tweenStopped = false
196
+ passenger.userData._rotTween = {
197
+ stop: () => {
198
+ tweenStopped = true
199
+ }
200
+ }
201
+
202
+ applyRemoteBoatPassengerRotation({
203
+ passenger,
204
+ vehicle: boat,
205
+ vehicleYaw: BOAT_YAW,
206
+ networkHeadYaw: lookYaw
207
+ })
208
+
209
+ expect(tweenStopped).toBe(true)
210
+ expect(passenger.userData._rotTween).toBeUndefined()
211
+ })
212
+
213
+ test('remote boat rotation clamps beyond ±105°', () => {
214
+ const lookYaw = BOAT_YAW + 2.0
215
+ const passenger = makeRemotePassenger(REMOTE_PLAYER_A, lookYaw)
216
+ const boat = makeBoat([REMOTE_PLAYER_A])
217
+ storeNetworkHeadYaw(passenger.userData, lookYaw)
218
+
219
+ applyRemoteBoatPassengerRotation({
220
+ passenger,
221
+ vehicle: boat,
222
+ vehicleYaw: BOAT_YAW,
223
+ networkHeadYaw: lookYaw
224
+ })
225
+
226
+ expect(passenger.playerObject!.skin.head.rotation.y).toBeCloseTo(BOAT_PASSENGER_MAX_HEAD_YAW_RAD)
227
+ })
@@ -0,0 +1,128 @@
1
+ //@ts-nocheck
2
+ import type * as THREE from 'three'
3
+ import type { PlayerObjectType } from '../../lib/createPlayerObject'
4
+ import { resolveBoatPassengerThirdPersonRotation, shouldApplyBoatPassengerRotation } from './boatPassengerRotation'
5
+ import { getNetworkHeadPitch, restoreGenericRemotePlayerHeadRotation, type NetworkHeadRotationState } from './networkHeadPitchRendering'
6
+
7
+ export type RemoteBoatPassengerEntity = {
8
+ rotation: { y: number; set: (x: number, y: number, z: number) => unknown }
9
+ visible: boolean
10
+ playerObject?: PlayerObjectType
11
+ userData: NetworkHeadRotationState & {
12
+ _passengerVehicleId?: string
13
+ _boatPassengerVehicleId?: string
14
+ _rotTween?: { stop: () => unknown }
15
+ }
16
+ }
17
+
18
+ export type RemoteBoatPassengerVehicle = {
19
+ rotation: { y: number }
20
+ realName?: string
21
+ originalEntity: { name: string }
22
+ }
23
+
24
+ export function getAnchoredVehicleId(userData: RemoteBoatPassengerEntity['userData']): string | undefined {
25
+ return userData._passengerVehicleId ?? userData._boatPassengerVehicleId
26
+ }
27
+
28
+ export function stopRemotePassengerRotationTween(userData: RemoteBoatPassengerEntity['userData']): void {
29
+ userData._rotTween?.stop()
30
+ userData._rotTween = undefined
31
+ }
32
+
33
+ export function applyRemoteBoatPassengerRotation(params: {
34
+ passenger: RemoteBoatPassengerEntity
35
+ vehicle: RemoteBoatPassengerVehicle
36
+ vehicleYaw: number
37
+ networkHeadYaw: number
38
+ }): void {
39
+ const { passenger, networkHeadYaw, vehicleYaw } = params
40
+ const playerObject = passenger.playerObject
41
+ if (!playerObject) return
42
+
43
+ stopRemotePassengerRotationTween(passenger.userData)
44
+
45
+ const resolved = resolveBoatPassengerThirdPersonRotation({
46
+ cameraYaw: networkHeadYaw,
47
+ cameraPitch: getNetworkHeadPitch(passenger.userData),
48
+ vehicleYaw
49
+ })
50
+
51
+ passenger.rotation.set(0, resolved.bodyYaw, 0)
52
+ playerObject.skin.head.rotation.set(resolved.headPitch, resolved.headYaw, 0)
53
+ passenger.userData._remoteBoatRotationApplied = true
54
+ }
55
+
56
+ export function restoreGenericRemotePassengerRotation(passenger: RemoteBoatPassengerEntity): void {
57
+ const playerObject = passenger.playerObject
58
+ if (!playerObject) return
59
+
60
+ stopRemotePassengerRotationTween(passenger.userData)
61
+
62
+ const networkHeadYaw = passenger.userData._networkHeadYaw
63
+ if (typeof networkHeadYaw === 'number' && Number.isFinite(networkHeadYaw)) {
64
+ passenger.rotation.set(0, networkHeadYaw, 0)
65
+ }
66
+
67
+ restoreGenericRemotePlayerHeadRotation(playerObject, passenger.userData)
68
+ passenger.userData._remoteBoatRotationApplied = false
69
+ }
70
+
71
+ export function processRemoteBoatPassengerRotation(params: {
72
+ passenger: RemoteBoatPassengerEntity
73
+ vehicle: RemoteBoatPassengerVehicle | undefined
74
+ syncArmor?: () => void
75
+ }): void {
76
+ const { passenger, vehicle, syncArmor } = params
77
+ if (!passenger.playerObject) return
78
+
79
+ const userData = passenger.userData
80
+ const wasApplied = userData._remoteBoatRotationApplied === true
81
+ const anchoredVehicleId = getAnchoredVehicleId(userData)
82
+ const vehicleName = vehicle?.realName ?? vehicle?.originalEntity.name
83
+ const vehicleYaw = vehicle?.rotation.y
84
+ const gatePasses = shouldApplyBoatPassengerRotation({
85
+ isAnchoredPassenger: anchoredVehicleId != null && vehicle != null,
86
+ vehicleName,
87
+ vehicleYaw
88
+ })
89
+
90
+ if (!gatePasses) {
91
+ if (wasApplied) {
92
+ restoreGenericRemotePassengerRotation(passenger)
93
+ syncArmor?.()
94
+ }
95
+ return
96
+ }
97
+
98
+ const networkHeadYaw = userData._networkHeadYaw
99
+ if (typeof networkHeadYaw !== 'number' || !Number.isFinite(networkHeadYaw)) {
100
+ return
101
+ }
102
+
103
+ applyRemoteBoatPassengerRotation({
104
+ passenger,
105
+ vehicle: vehicle!,
106
+ vehicleYaw: vehicleYaw!,
107
+ networkHeadYaw
108
+ })
109
+ syncArmor?.()
110
+ }
111
+
112
+ export function processRemoteBoatPassengerRotations(params: {
113
+ entities: Record<string, RemoteBoatPassengerEntity | undefined>
114
+ syncArmor: (passenger: RemoteBoatPassengerEntity) => void
115
+ }): void {
116
+ for (const entity of Object.values(params.entities)) {
117
+ if (!entity?.playerObject) continue
118
+
119
+ const anchoredVehicleId = getAnchoredVehicleId(entity.userData)
120
+ const vehicle = anchoredVehicleId != null ? (params.entities[anchoredVehicleId] as RemoteBoatPassengerVehicle | undefined) : undefined
121
+
122
+ processRemoteBoatPassengerRotation({
123
+ passenger: entity,
124
+ vehicle,
125
+ syncArmor: entity.visible ? () => params.syncArmor(entity) : undefined
126
+ })
127
+ }
128
+ }
@@ -0,0 +1,214 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import { expect, test } from 'vitest'
4
+ import { SceneOrigin } from '../sceneOrigin'
5
+ import { getBoatPassengerWorldPosition, getMinecartPassengerWorldPosition } from './interpolationPolicy'
6
+ import { anchorVehiclePassengerPosition, shouldSkipLocalPassengerAnchoring } from './vehiclePassengerRendering'
7
+ import { updateVehiclePassengerPositions, type VehiclePassengerSceneEntity, type VehiclePassengerVehicle } from './vehiclePassengerUpdate'
8
+
9
+ const LOCAL_PLAYER_ID = 7
10
+ const REMOTE_PLAYER_ID = 8
11
+
12
+ type PassengerLike = THREE.Group & VehiclePassengerSceneEntity
13
+
14
+ function makePassenger(id: number): PassengerLike {
15
+ const passenger = new THREE.Group() as PassengerLike
16
+ passenger.playerObject = {}
17
+ passenger.originalEntity = { id }
18
+ passenger.userData = {}
19
+ passenger.rotation.y = 0
20
+ return passenger
21
+ }
22
+
23
+ function makeVehicle(id: number, name: string, renderHints: Record<string, unknown>): VehiclePassengerVehicle & THREE.Group {
24
+ const vehicle = new THREE.Group() as VehiclePassengerVehicle & THREE.Group
25
+ vehicle.originalEntity = { id, name, height: name === 'donkey' ? 1.5 : 1.6 }
26
+ vehicle.userData = { renderHints }
27
+ vehicle.realName = name
28
+ vehicle.rotation.y = 0
29
+ return vehicle
30
+ }
31
+
32
+ function createHarness() {
33
+ const sceneOrigin = new SceneOrigin(new THREE.Scene())
34
+ sceneOrigin.update(96, 60, 192)
35
+
36
+ const localPlayer = makePassenger(LOCAL_PLAYER_ID)
37
+ sceneOrigin.track(localPlayer)
38
+
39
+ const entities: Record<string, VehiclePassengerSceneEntity | VehiclePassengerVehicle> = {}
40
+ const cameraWorldPos = { x: 100, y: 65.62, z: 200 }
41
+
42
+ const runUpdate = () =>
43
+ updateVehiclePassengerPositions({
44
+ entities,
45
+ localPlayer,
46
+ getWorldPosition: target => sceneOrigin.getWorldPosition(target as THREE.Object3D)
47
+ })
48
+
49
+ return { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate }
50
+ }
51
+
52
+ test('shouldSkipLocalPassengerAnchoring keeps horse on camera path', () => {
53
+ expect(shouldSkipLocalPassengerAnchoring('boat')).toBe(false)
54
+ expect(shouldSkipLocalPassengerAnchoring('minecart')).toBe(false)
55
+ expect(shouldSkipLocalPassengerAnchoring('horse')).toBe(true)
56
+ })
57
+
58
+ test('updateVehiclePassengerPositions anchors local boat and minecart passengers', () => {
59
+ const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
60
+
61
+ const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
62
+ sceneOrigin.track(boat)
63
+ boat.position.set(100, 64, 200)
64
+ entities['10'] = boat
65
+
66
+ runUpdate()
67
+
68
+ const expectedBoatSeat = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
69
+ expect(localPlayer.userData._passengerVehicleId).toBe('10')
70
+ expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(expectedBoatSeat)
71
+
72
+ const minecart = makeVehicle(42, 'minecart', { passengerLayout: 'minecart', passengerIds: [LOCAL_PLAYER_ID] })
73
+ sceneOrigin.track(minecart)
74
+ minecart.position.set(103, 64.5, 205)
75
+ entities['42'] = minecart
76
+ boat.userData.renderHints = { passengerLayout: 'boat', passengerIds: [] }
77
+
78
+ runUpdate()
79
+
80
+ const expectedMinecartSeat = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
81
+ expect(localPlayer.userData._passengerVehicleId).toBe('42')
82
+ expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(expectedMinecartSeat)
83
+ })
84
+
85
+ test('updateVehiclePassengerPositions keeps local horse on existing skip path', () => {
86
+ const { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate } = createHarness()
87
+ const eyeHeight = 1.62
88
+ localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - eyeHeight, cameraWorldPos.z)
89
+
90
+ const horse = makeVehicle(42, 'horse', {
91
+ passengerLayout: 'horse',
92
+ passengerIds: [LOCAL_PLAYER_ID],
93
+ localVehicle: true,
94
+ localVehicleVerticalCameraLock: 'horse'
95
+ })
96
+ sceneOrigin.track(horse)
97
+ horse.position.set(100, 64, 200)
98
+ entities['42'] = horse
99
+
100
+ runUpdate()
101
+
102
+ expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
103
+ expect(sceneOrigin.getWorldPosition(localPlayer)?.y).toBeCloseTo(cameraWorldPos.y - eyeHeight, 5)
104
+ })
105
+
106
+ test('local and remote boat passengers use the same seat algorithm per index', () => {
107
+ const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
108
+ const remotePlayer = makePassenger(REMOTE_PLAYER_ID)
109
+ sceneOrigin.track(remotePlayer)
110
+ entities[String(REMOTE_PLAYER_ID)] = remotePlayer
111
+
112
+ const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID, REMOTE_PLAYER_ID] })
113
+ sceneOrigin.track(boat)
114
+ boat.position.set(100, 64, 200)
115
+ entities['10'] = boat
116
+
117
+ runUpdate()
118
+
119
+ const vehicleWorldPos = sceneOrigin.getWorldPosition(boat)!
120
+ const localWorld = sceneOrigin.getWorldPosition(localPlayer)!
121
+ const remoteWorld = sceneOrigin.getWorldPosition(remotePlayer)!
122
+ expect(localWorld).toEqual(getBoatPassengerWorldPosition(vehicleWorldPos, boat.rotation.y, 0, 2))
123
+ expect(remoteWorld).toEqual(getBoatPassengerWorldPosition(vehicleWorldPos, boat.rotation.y, 1, 2))
124
+ expect(localWorld.y).toBeCloseTo(63.55, 5)
125
+ expect(remoteWorld.y).toBeCloseTo(63.55, 5)
126
+ })
127
+
128
+ test('vehicle movement updates body anchor without horizontal jitter across frames', () => {
129
+ const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
130
+
131
+ const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID], localVehicle: true })
132
+ sceneOrigin.track(boat)
133
+ entities['10'] = boat
134
+
135
+ const xs = [100, 100.4, 100.9, 101.1, 100.8]
136
+ const deltas: number[] = []
137
+ let previousX: number | undefined
138
+
139
+ for (const x of xs) {
140
+ boat.position.set(x, 64, 200)
141
+ runUpdate()
142
+ const world = sceneOrigin.getWorldPosition(localPlayer)!
143
+ if (previousX !== undefined) {
144
+ deltas.push(world.x - previousX)
145
+ }
146
+ previousX = world.x
147
+ }
148
+
149
+ for (const delta of deltas) {
150
+ expect(delta).toBeCloseTo(Math.round(delta * 10) / 10, 5)
151
+ }
152
+ expect(deltas.map(delta => Math.round(delta * 10) / 10)).toEqual([0.4, 0.5, 0.2, -0.3])
153
+ })
154
+
155
+ test('dismount and vehicle removal release local passenger anchor', () => {
156
+ const { sceneOrigin, localPlayer, entities, cameraWorldPos, runUpdate } = createHarness()
157
+ const eyeHeight = 1.62
158
+ localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - eyeHeight, cameraWorldPos.z)
159
+
160
+ const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
161
+ sceneOrigin.track(boat)
162
+ boat.position.set(100, 64, 200)
163
+ entities['10'] = boat
164
+
165
+ runUpdate()
166
+ expect(localPlayer.userData._passengerVehicleId).toBe('10')
167
+
168
+ boat.userData.renderHints = { passengerLayout: 'boat', passengerIds: [] }
169
+ runUpdate()
170
+ expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
171
+
172
+ localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - eyeHeight, cameraWorldPos.z)
173
+ const releasedWorld = sceneOrigin.getWorldPosition(localPlayer)!
174
+ expect(releasedWorld.y).toBeCloseTo(cameraWorldPos.y - eyeHeight, 5)
175
+
176
+ delete entities['10']
177
+ runUpdate()
178
+ expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
179
+ })
180
+
181
+ test('scene origin rebase preserves passenger world position', () => {
182
+ const { sceneOrigin, localPlayer, entities, runUpdate } = createHarness()
183
+
184
+ const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
185
+ sceneOrigin.track(boat)
186
+ boat.position.set(100, 64, 200)
187
+ entities['10'] = boat
188
+
189
+ runUpdate()
190
+ const beforeRebase = { ...sceneOrigin.getWorldPosition(localPlayer)! }
191
+
192
+ sceneOrigin.update(112, 70, 208)
193
+ runUpdate()
194
+ const afterRebase = sceneOrigin.getWorldPosition(localPlayer)!
195
+
196
+ expect(afterRebase).toEqual(beforeRebase)
197
+ })
198
+
199
+ test('non-finite vehicle position does not create passenger anchor', () => {
200
+ const { localPlayer, entities, runUpdate } = createHarness()
201
+
202
+ const boat = makeVehicle(10, 'oak_boat', { passengerLayout: 'boat', passengerIds: [LOCAL_PLAYER_ID] })
203
+ boat.position.set(Number.NaN, 64, 200)
204
+ entities['10'] = boat
205
+
206
+ runUpdate()
207
+ expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
208
+ })
209
+
210
+ test('anchorVehiclePassengerPosition ignores non-finite coordinates', () => {
211
+ const passenger = makePassenger(LOCAL_PLAYER_ID)
212
+ anchorVehiclePassengerPosition(passenger, { x: 1, y: Number.NaN, z: 2 }, '10')
213
+ expect(passenger.userData._passengerVehicleId).toBeUndefined()
214
+ })
@@ -0,0 +1,56 @@
1
+ //@ts-nocheck
2
+ import type { Vec3Like } from './interpolationPolicy'
3
+
4
+ export type PassengerLayout = 'boat' | 'minecart' | 'horse'
5
+
6
+ export function shouldSkipLocalPassengerAnchoring(layout: PassengerLayout): boolean {
7
+ return layout === 'horse'
8
+ }
9
+
10
+ export function isFiniteVec3(value: Vec3Like | undefined | null): value is Vec3Like {
11
+ return value != null && Number.isFinite(value.x) && Number.isFinite(value.y) && Number.isFinite(value.z)
12
+ }
13
+
14
+ export type VehiclePassengerRenderState = {
15
+ position: {
16
+ set: (x: number, y: number, z: number) => unknown
17
+ }
18
+ userData: {
19
+ _posTween?: { stop: () => unknown }
20
+ _tweenTarget?: Vec3Like
21
+ _passengerVehicleId?: string
22
+ /** @deprecated Use _passengerVehicleId */
23
+ _boatPassengerVehicleId?: string
24
+ }
25
+ }
26
+
27
+ export function anchorVehiclePassengerPosition(passenger: VehiclePassengerRenderState, passengerWorldPos: Vec3Like, vehicleId: string): void {
28
+ if (!isFiniteVec3(passengerWorldPos)) return
29
+ passenger.userData._posTween?.stop()
30
+ passenger.userData._posTween = undefined
31
+ passenger.userData._tweenTarget ??= { ...passengerWorldPos }
32
+ Object.assign(passenger.userData._tweenTarget, passengerWorldPos)
33
+ passenger.position.set(passengerWorldPos.x, passengerWorldPos.y, passengerWorldPos.z)
34
+ passenger.userData._passengerVehicleId = vehicleId
35
+ delete passenger.userData._boatPassengerVehicleId
36
+ }
37
+
38
+ export function releaseVehiclePassengerPosition(passenger: VehiclePassengerRenderState, vehicleId: string, currentWorldPos: Vec3Like | undefined): boolean {
39
+ const anchoredVehicleId = passenger.userData._passengerVehicleId ?? passenger.userData._boatPassengerVehicleId
40
+ if (anchoredVehicleId !== vehicleId) return false
41
+ passenger.userData._posTween?.stop()
42
+ passenger.userData._posTween = undefined
43
+ if (currentWorldPos) {
44
+ passenger.userData._tweenTarget ??= { ...currentWorldPos }
45
+ Object.assign(passenger.userData._tweenTarget, currentWorldPos)
46
+ }
47
+ delete passenger.userData._passengerVehicleId
48
+ delete passenger.userData._boatPassengerVehicleId
49
+ return true
50
+ }
51
+
52
+ /** @deprecated Use anchorVehiclePassengerPosition */
53
+ export const anchorBoatPassengerPosition = anchorVehiclePassengerPosition
54
+
55
+ /** @deprecated Use releaseVehiclePassengerPosition */
56
+ export const releaseBoatPassengerPosition = releaseVehiclePassengerPosition