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.
- package/dist/minecraft-renderer.js +68 -68
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +522 -522
- package/package.json +1 -1
- package/src/graphicsBackend/appViewer.ts +4 -3
- package/src/graphicsBackend/config.ts +1 -1
- package/src/graphicsBackend/types.ts +4 -1
- package/src/lib/worldrendererCommon.ts +3 -3
- package/src/three/entities.ts +329 -43
- package/src/three/entity/EntityMesh.ts +8 -3
- package/src/three/entity/animations.js +37 -25
- package/src/three/entity/animations.test.ts +249 -0
- package/src/three/entity/boatModelRotation.test.ts +29 -0
- package/src/three/entity/boatModelRotation.ts +14 -0
- package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
- package/src/three/entity/boatPaddleAnimation.ts +104 -0
- package/src/three/entity/boatPassengerRendering.ts +20 -0
- package/src/three/entity/boatPassengerRotation.test.ts +166 -0
- package/src/three/entity/boatPassengerRotation.ts +71 -0
- package/src/three/entity/boatRenderSetup.test.ts +236 -0
- package/src/three/entity/boatRenderSetup.ts +214 -0
- package/src/three/entity/entityHeadPose.test.ts +15 -0
- package/src/three/entity/entityHeadPose.ts +22 -0
- package/src/three/entity/horseEntityScale.test.ts +11 -0
- package/src/three/entity/horseEntityScale.ts +9 -0
- package/src/three/entity/horseHeadAnimation.test.ts +159 -0
- package/src/three/entity/horseHeadAnimation.ts +151 -0
- package/src/three/entity/horsePassengerRendering.test.ts +77 -0
- package/src/three/entity/interpolationPolicy.test.ts +259 -0
- package/src/three/entity/interpolationPolicy.ts +187 -0
- package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
- package/src/three/entity/localVehicleRendering.integration.test.ts +247 -0
- package/src/three/entity/minecartLocalPassengerRendering.test.ts +208 -0
- package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
- package/src/three/entity/networkHeadPitchRendering.ts +44 -0
- package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +227 -0
- package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
- package/src/three/entity/vehiclePassengerProduction.test.ts +214 -0
- package/src/three/entity/vehiclePassengerRendering.ts +56 -0
- package/src/three/entity/vehiclePassengerUpdate.ts +82 -0
- package/src/three/graphicsBackendBase.ts +5 -5
- package/src/three/graphicsBackendOffThread.ts +2 -2
- package/src/three/worldRendererThree.ts +54 -27
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
ENTITY_TWEEN_DURATION_MS,
|
|
5
|
+
LOCAL_MOVEMENT_TWEEN_DURATION_MS,
|
|
6
|
+
SPECTATING_CAMERA_TWEEN_DURATION_MS,
|
|
7
|
+
getCameraMovementTweenDurationMs,
|
|
8
|
+
getEntityRotationTweenDurationMs,
|
|
9
|
+
getEntityTweenDurationMs,
|
|
10
|
+
getHorsePassengerWorldPosition,
|
|
11
|
+
getLocalVehicleWorldPosition,
|
|
12
|
+
getMinecartPassengerWorldPosition,
|
|
13
|
+
resolveLocalVehicleWorldPosition,
|
|
14
|
+
samePosition,
|
|
15
|
+
shouldRestartCameraPositionTween,
|
|
16
|
+
usesCameraSyncedVehiclePosition
|
|
17
|
+
} from './interpolationPolicy'
|
|
18
|
+
|
|
19
|
+
test('local movement tween matches camera tween duration', () => {
|
|
20
|
+
expect(LOCAL_MOVEMENT_TWEEN_DURATION_MS).toBe(50)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('ordinary entity tween stays at 120 ms', () => {
|
|
24
|
+
expect(ENTITY_TWEEN_DURATION_MS).toBe(120)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('server-vehicle camera uses entity tween duration', () => {
|
|
28
|
+
expect(getCameraMovementTweenDurationMs('server-vehicle')).toBe(120)
|
|
29
|
+
expect(getCameraMovementTweenDurationMs('local-player')).toBe(50)
|
|
30
|
+
expect(getCameraMovementTweenDurationMs('spectating')).toBe(SPECTATING_CAMERA_TWEEN_DURATION_MS)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('forced teleport camera update is instant', () => {
|
|
34
|
+
expect(getCameraMovementTweenDurationMs('server-vehicle', true)).toBe(0)
|
|
35
|
+
expect(getCameraMovementTweenDurationMs('local-player', true)).toBe(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('same target does not restart camera tween', () => {
|
|
39
|
+
const target = { x: 1, y: 63.7, z: 2 }
|
|
40
|
+
expect(
|
|
41
|
+
shouldRestartCameraPositionTween({
|
|
42
|
+
target,
|
|
43
|
+
currentTarget: { ...target },
|
|
44
|
+
movementMode: 'server-vehicle',
|
|
45
|
+
previousMovementMode: 'server-vehicle',
|
|
46
|
+
instant: false
|
|
47
|
+
})
|
|
48
|
+
).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('dismount switches camera tween mode back to local-player', () => {
|
|
52
|
+
const target = { x: 1, y: 63.7, z: 2 }
|
|
53
|
+
expect(
|
|
54
|
+
shouldRestartCameraPositionTween({
|
|
55
|
+
target,
|
|
56
|
+
currentTarget: { ...target },
|
|
57
|
+
movementMode: 'local-player',
|
|
58
|
+
previousMovementMode: 'server-vehicle',
|
|
59
|
+
instant: false
|
|
60
|
+
})
|
|
61
|
+
).toBe(true)
|
|
62
|
+
expect(getCameraMovementTweenDurationMs('local-player')).toBe(50)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('sequential minecart targets restart tween only when position changes', () => {
|
|
66
|
+
const first = { x: 0, y: 63.7, z: 0 }
|
|
67
|
+
const second = { x: 1, y: 63.7, z: 0 }
|
|
68
|
+
expect(
|
|
69
|
+
shouldRestartCameraPositionTween({
|
|
70
|
+
target: first,
|
|
71
|
+
currentTarget: null,
|
|
72
|
+
movementMode: 'server-vehicle',
|
|
73
|
+
previousMovementMode: null,
|
|
74
|
+
instant: false
|
|
75
|
+
})
|
|
76
|
+
).toBe(true)
|
|
77
|
+
expect(
|
|
78
|
+
shouldRestartCameraPositionTween({
|
|
79
|
+
target: first,
|
|
80
|
+
currentTarget: first,
|
|
81
|
+
movementMode: 'server-vehicle',
|
|
82
|
+
previousMovementMode: 'server-vehicle',
|
|
83
|
+
instant: false
|
|
84
|
+
})
|
|
85
|
+
).toBe(false)
|
|
86
|
+
expect(
|
|
87
|
+
shouldRestartCameraPositionTween({
|
|
88
|
+
target: second,
|
|
89
|
+
currentTarget: first,
|
|
90
|
+
movementMode: 'server-vehicle',
|
|
91
|
+
previousMovementMode: 'server-vehicle',
|
|
92
|
+
instant: false
|
|
93
|
+
})
|
|
94
|
+
).toBe(true)
|
|
95
|
+
expect(second.x).toBeGreaterThan(first.x)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('samePosition uses epsilon comparison', () => {
|
|
99
|
+
expect(samePosition({ x: 1, y: 2, z: 3 }, { x: 1.00001, y: 2.00001, z: 3.00001 })).toBe(true)
|
|
100
|
+
expect(samePosition({ x: 1, y: 2, z: 3 }, { x: 1.01, y: 2, z: 3 })).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('local vehicle skips position tween', () => {
|
|
104
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { localVehicle: true } })).toBe(true)
|
|
105
|
+
expect(getEntityTweenDurationMs({ renderHints: { localVehicle: true } }, false)).toBe(0)
|
|
106
|
+
expect(getEntityTweenDurationMs({ renderHints: { localVehicle: true } }, true)).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('local vehicle skips rotation tween while remote vehicles keep it', () => {
|
|
110
|
+
expect(getEntityRotationTweenDurationMs({ renderHints: { localVehicle: true } }, false)).toBe(0)
|
|
111
|
+
expect(getEntityRotationTweenDurationMs({ renderHints: { localVehicle: false } }, false)).toBe(ENTITY_TWEEN_DURATION_MS)
|
|
112
|
+
expect(getEntityRotationTweenDurationMs(undefined, true)).toBe(0)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('locally ridden vehicle camera-sync policy stays separate from server-vehicle mode', () => {
|
|
116
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { localVehicle: true } })).toBe(true)
|
|
117
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { localVehicle: true, passengerLayout: 'minecart' } })).toBe(true)
|
|
118
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { passengerLayout: 'minecart' } })).toBe(false)
|
|
119
|
+
expect(getCameraMovementTweenDurationMs('local-player')).toBe(50)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('remote entities keep ordinary tween duration', () => {
|
|
123
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { localVehicle: false } })).toBe(false)
|
|
124
|
+
expect(getEntityTweenDurationMs({ renderHints: { localVehicle: false } }, false)).toBe(120)
|
|
125
|
+
expect(getEntityTweenDurationMs({}, false)).toBe(120)
|
|
126
|
+
expect(getEntityTweenDurationMs(undefined, false)).toBe(120)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('local vehicle world position uses camera X/Z and vehicle Y', () => {
|
|
130
|
+
const camera = { x: 10, y: 64.5, z: -3 }
|
|
131
|
+
expect(getLocalVehicleWorldPosition(camera, 63.2)).toEqual({
|
|
132
|
+
x: 10,
|
|
133
|
+
y: 63.2,
|
|
134
|
+
z: -3
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('intermediate camera frames keep boat X/Z aligned with player', () => {
|
|
139
|
+
const vehicleY = 63.2
|
|
140
|
+
const start = getLocalVehicleWorldPosition({ x: 0, y: 64, z: 0 }, vehicleY)
|
|
141
|
+
const mid = getLocalVehicleWorldPosition({ x: 0.5, y: 64, z: 0 }, vehicleY)
|
|
142
|
+
const end = getLocalVehicleWorldPosition({ x: 1, y: 64, z: 0 }, vehicleY)
|
|
143
|
+
|
|
144
|
+
expect(start.x).toBe(0)
|
|
145
|
+
expect(mid.x).toBe(0.5)
|
|
146
|
+
expect(end.x).toBe(1)
|
|
147
|
+
expect(start.z).toBe(mid.z)
|
|
148
|
+
expect(mid.z).toBe(end.z)
|
|
149
|
+
expect(start.y).toBe(vehicleY)
|
|
150
|
+
expect(mid.y).toBe(vehicleY)
|
|
151
|
+
expect(end.y).toBe(vehicleY)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('horse vertical camera lock keeps constant offset from camera across tween progress', () => {
|
|
155
|
+
const eyeHeight = 1.62
|
|
156
|
+
const feetOffset = 0.85
|
|
157
|
+
const expectedGap = -(eyeHeight + feetOffset)
|
|
158
|
+
const rawVehicleY = 64
|
|
159
|
+
const cameraStartY = rawVehicleY + feetOffset + eyeHeight
|
|
160
|
+
const tweenProgress = [0, 0.25, 0.5, 0.75, 1]
|
|
161
|
+
|
|
162
|
+
for (const progress of tweenProgress) {
|
|
163
|
+
const cameraY = cameraStartY + progress * 0.8
|
|
164
|
+
const resolved = resolveLocalVehicleWorldPosition({
|
|
165
|
+
cameraWorldPos: { x: 1 + progress, y: cameraY, z: 2 },
|
|
166
|
+
rawVehicleY,
|
|
167
|
+
eyeHeight,
|
|
168
|
+
vehicleName: 'horse',
|
|
169
|
+
vehicleHeight: 1.6,
|
|
170
|
+
verticalCameraLock: 'horse'
|
|
171
|
+
})
|
|
172
|
+
expect(resolved.y - cameraY).toBeCloseTo(expectedGap, 5)
|
|
173
|
+
expect(resolved.x).toBe(1 + progress)
|
|
174
|
+
expect(resolved.z).toBe(2)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('horse vertical camera lock uses vanilla feet offset value', () => {
|
|
179
|
+
const eyeHeight = 1.62
|
|
180
|
+
const cameraY = 66.47
|
|
181
|
+
const resolved = resolveLocalVehicleWorldPosition({
|
|
182
|
+
cameraWorldPos: { x: 0, y: cameraY, z: 0 },
|
|
183
|
+
rawVehicleY: 64,
|
|
184
|
+
eyeHeight,
|
|
185
|
+
vehicleName: 'horse',
|
|
186
|
+
vehicleHeight: 1.6,
|
|
187
|
+
verticalCameraLock: 'horse'
|
|
188
|
+
})
|
|
189
|
+
expect(resolved.y).toBeCloseTo(cameraY - 2.47, 5)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('horse vertical camera lock is variant-aware for donkey and skeleton_horse', () => {
|
|
193
|
+
const eyeHeight = 1.62
|
|
194
|
+
const cameraY = 65
|
|
195
|
+
|
|
196
|
+
const donkey = resolveLocalVehicleWorldPosition({
|
|
197
|
+
cameraWorldPos: { x: 0, y: cameraY, z: 0 },
|
|
198
|
+
rawVehicleY: 64,
|
|
199
|
+
eyeHeight,
|
|
200
|
+
vehicleName: 'donkey',
|
|
201
|
+
vehicleHeight: 1.6,
|
|
202
|
+
verticalCameraLock: 'horse'
|
|
203
|
+
})
|
|
204
|
+
expect(donkey.y).toBeCloseTo(cameraY - eyeHeight - 0.6, 5)
|
|
205
|
+
|
|
206
|
+
const skeleton = resolveLocalVehicleWorldPosition({
|
|
207
|
+
cameraWorldPos: { x: 0, y: cameraY, z: 0 },
|
|
208
|
+
rawVehicleY: 64,
|
|
209
|
+
eyeHeight,
|
|
210
|
+
vehicleName: 'skeleton_horse',
|
|
211
|
+
vehicleHeight: 1.6,
|
|
212
|
+
verticalCameraLock: 'horse'
|
|
213
|
+
})
|
|
214
|
+
expect(skeleton.y).toBeCloseTo(cameraY - eyeHeight - 0.6625, 5)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('without vertical camera lock resolver keeps raw vehicle Y', () => {
|
|
218
|
+
const camera = { x: 10, y: 64.5, z: -3 }
|
|
219
|
+
const rawVehicleY = 63.2
|
|
220
|
+
expect(
|
|
221
|
+
resolveLocalVehicleWorldPosition({
|
|
222
|
+
cameraWorldPos: camera,
|
|
223
|
+
rawVehicleY,
|
|
224
|
+
eyeHeight: 1.62,
|
|
225
|
+
vehicleName: 'horse',
|
|
226
|
+
vehicleHeight: 1.6
|
|
227
|
+
})
|
|
228
|
+
).toEqual(getLocalVehicleWorldPosition(camera, rawVehicleY))
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('horse vertical camera lock falls back to raw Y when result is non-finite', () => {
|
|
232
|
+
const camera = { x: 1, y: 64, z: 2 }
|
|
233
|
+
const rawVehicleY = 63.5
|
|
234
|
+
expect(
|
|
235
|
+
resolveLocalVehicleWorldPosition({
|
|
236
|
+
cameraWorldPos: camera,
|
|
237
|
+
rawVehicleY,
|
|
238
|
+
eyeHeight: Number.NaN,
|
|
239
|
+
vehicleName: 'horse',
|
|
240
|
+
vehicleHeight: 1.6,
|
|
241
|
+
verticalCameraLock: 'horse'
|
|
242
|
+
})
|
|
243
|
+
).toEqual(getLocalVehicleWorldPosition(camera, rawVehicleY))
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('minecart passenger feet Y is vehicleY - 0.35', () => {
|
|
247
|
+
expect(getMinecartPassengerWorldPosition({ x: 0, y: 64, z: 0 }).y).toBeCloseTo(63.65, 5)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test.each([
|
|
251
|
+
['horse', 1.6, 0.85],
|
|
252
|
+
['zombie_horse', 1.6, 0.85],
|
|
253
|
+
['donkey', 1.5, 0.525],
|
|
254
|
+
['mule', 1.6, 0.6],
|
|
255
|
+
['skeleton_horse', 1.6, 0.6625]
|
|
256
|
+
])('horse variant %s feet offset matches vanilla 1.17.1', (name, height, expectedFeetOffset) => {
|
|
257
|
+
const seat = getHorsePassengerWorldPosition({ x: 0, y: 64, z: 0 }, name, height)
|
|
258
|
+
expect(seat.y).toBeCloseTo(64 + expectedFeetOffset, 5)
|
|
259
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export const LOCAL_MOVEMENT_TWEEN_DURATION_MS = 50
|
|
3
|
+
export const ENTITY_TWEEN_DURATION_MS = 120
|
|
4
|
+
export const SPECTATING_CAMERA_TWEEN_DURATION_MS = 150
|
|
5
|
+
export const CAMERA_POSITION_EPSILON = 1e-4
|
|
6
|
+
|
|
7
|
+
export type CameraMovementMode = 'local-player' | 'server-vehicle' | 'spectating'
|
|
8
|
+
|
|
9
|
+
export type UpdateCameraOptions = {
|
|
10
|
+
movementMode?: CameraMovementMode
|
|
11
|
+
instant?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Vec3Like = { x: number; y: number; z: number }
|
|
15
|
+
|
|
16
|
+
export type EntityRenderHints = {
|
|
17
|
+
localVehicle?: boolean
|
|
18
|
+
localVehicleVerticalCameraLock?: 'horse'
|
|
19
|
+
/** Render-only: lock locally controlled horse yaw to camera each frame. */
|
|
20
|
+
localVehicleYawLock?: 'horse'
|
|
21
|
+
boatWaterPatchVisible?: boolean
|
|
22
|
+
boatPaddleLeft?: boolean
|
|
23
|
+
boatPaddleRight?: boolean
|
|
24
|
+
passengerIds?: number[]
|
|
25
|
+
passengerLayout?: 'boat' | 'minecart' | 'horse'
|
|
26
|
+
/** @deprecated Use passengerIds */
|
|
27
|
+
boatPassengerIds?: number[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type EntityWithRenderHints = {
|
|
31
|
+
renderHints?: EntityRenderHints
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function usesCameraSyncedVehiclePosition(entity: EntityWithRenderHints | undefined): boolean {
|
|
35
|
+
return !!entity?.renderHints?.localVehicle
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function shouldApplyLocalHorseCameraYawLock(renderHints: EntityRenderHints | undefined): boolean {
|
|
39
|
+
return renderHints?.localVehicleYawLock === 'horse'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Visual-only: align local horse model yaw with camera before head pose and passengers. */
|
|
43
|
+
export function applyLocalHorseCameraYawLock(sceneEntity: { rotation: { y: number }; userData: Record<string, unknown> }, cameraYaw: number): boolean {
|
|
44
|
+
if (!Number.isFinite(cameraYaw)) return false
|
|
45
|
+
sceneEntity.rotation.y = cameraYaw
|
|
46
|
+
sceneEntity.userData._horseBodyYaw = cameraYaw
|
|
47
|
+
sceneEntity.userData._horseHeadYaw = cameraYaw
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getEntityTweenDurationMs(entity: EntityWithRenderHints | undefined, justAdded: boolean): number {
|
|
52
|
+
if (justAdded) return 0
|
|
53
|
+
if (usesCameraSyncedVehiclePosition(entity)) return 0
|
|
54
|
+
return ENTITY_TWEEN_DURATION_MS
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Rotation follows the same instant-vs-remote policy as entity position. */
|
|
58
|
+
export function getEntityRotationTweenDurationMs(entity: EntityWithRenderHints | undefined, justAdded: boolean): number {
|
|
59
|
+
return getEntityTweenDurationMs(entity, justAdded)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function samePosition(a: Vec3Like, b: Vec3Like, epsilon = CAMERA_POSITION_EPSILON): boolean {
|
|
63
|
+
return Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon && Math.abs(a.z - b.z) < epsilon
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getCameraMovementTweenDurationMs(mode: CameraMovementMode, instant = false): number {
|
|
67
|
+
if (instant) return 0
|
|
68
|
+
switch (mode) {
|
|
69
|
+
case 'spectating':
|
|
70
|
+
return SPECTATING_CAMERA_TWEEN_DURATION_MS
|
|
71
|
+
case 'server-vehicle':
|
|
72
|
+
return ENTITY_TWEEN_DURATION_MS
|
|
73
|
+
case 'local-player':
|
|
74
|
+
default:
|
|
75
|
+
return LOCAL_MOVEMENT_TWEEN_DURATION_MS
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function shouldRestartCameraPositionTween(args: {
|
|
80
|
+
target: Vec3Like
|
|
81
|
+
currentTarget: Vec3Like | null
|
|
82
|
+
movementMode: CameraMovementMode
|
|
83
|
+
previousMovementMode: CameraMovementMode | null
|
|
84
|
+
instant: boolean
|
|
85
|
+
}): boolean {
|
|
86
|
+
if (args.instant) return true
|
|
87
|
+
if (args.currentTarget == null) return true
|
|
88
|
+
if (args.movementMode !== args.previousMovementMode) return true
|
|
89
|
+
return !samePosition(args.target, args.currentTarget)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Locally ridden vehicle X/Z follow camera tween; Y uses latest server vehicle height. */
|
|
93
|
+
export function getLocalVehicleWorldPosition(cameraWorldPos: Vec3Like, vehicleY: number): Vec3Like {
|
|
94
|
+
return {
|
|
95
|
+
x: cameraWorldPos.x,
|
|
96
|
+
y: vehicleY,
|
|
97
|
+
z: cameraWorldPos.z
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveLocalVehicleWorldPosition(args: {
|
|
102
|
+
cameraWorldPos: Vec3Like
|
|
103
|
+
rawVehicleY: number
|
|
104
|
+
eyeHeight: number
|
|
105
|
+
vehicleName: string | undefined
|
|
106
|
+
vehicleHeight: number
|
|
107
|
+
verticalCameraLock?: 'horse'
|
|
108
|
+
}): Vec3Like {
|
|
109
|
+
const { cameraWorldPos, rawVehicleY, eyeHeight, vehicleName, vehicleHeight, verticalCameraLock } = args
|
|
110
|
+
if (verticalCameraLock === 'horse') {
|
|
111
|
+
const y = cameraWorldPos.y - eyeHeight - getHorsePassengerFeetOffsetY(vehicleName, vehicleHeight)
|
|
112
|
+
if (Number.isFinite(y)) {
|
|
113
|
+
return { x: cameraWorldPos.x, y, z: cameraWorldPos.z }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return getLocalVehicleWorldPosition(cameraWorldPos, rawVehicleY)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const BOAT_PASSENGER_RIDING_OFFSET_Y = -0.1
|
|
120
|
+
const PLAYER_RIDING_OFFSET_Y = -0.35
|
|
121
|
+
/** Vanilla 1.17.1 AbstractMinecart#getPassengersRidingOffset() */
|
|
122
|
+
const MINECART_PASSENGER_RIDING_OFFSET_Y = 0
|
|
123
|
+
|
|
124
|
+
const RIDEABLE_MINECART_ENTITY_NAMES = new Set([
|
|
125
|
+
'minecart',
|
|
126
|
+
'chest_minecart',
|
|
127
|
+
'furnace_minecart',
|
|
128
|
+
'hopper_minecart',
|
|
129
|
+
'tnt_minecart',
|
|
130
|
+
'spawner_minecart',
|
|
131
|
+
'command_block_minecart'
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
const RIDEABLE_HORSE_ENTITY_NAMES = new Set(['horse', 'donkey', 'mule', 'skeleton_horse', 'zombie_horse'])
|
|
135
|
+
|
|
136
|
+
export function isRideableHorseEntityName(name?: string): boolean {
|
|
137
|
+
if (!name) return false
|
|
138
|
+
return RIDEABLE_HORSE_ENTITY_NAMES.has(name)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isRideableMinecartEntityName(name?: string): boolean {
|
|
142
|
+
if (!name) return false
|
|
143
|
+
return RIDEABLE_MINECART_ENTITY_NAMES.has(name)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getBoatPassengerSeatOffset(passengerIndex: number, passengerCount: number): number {
|
|
147
|
+
if (passengerCount <= 1) return 0
|
|
148
|
+
return passengerIndex === 0 ? 0.2 : -0.6
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Vanilla 1.17.1 Boat#positionRider position, without applying the passenger pose. */
|
|
152
|
+
export function getBoatPassengerWorldPosition(boatWorldPos: Vec3Like, boatYaw: number, passengerIndex: number, passengerCount: number): Vec3Like {
|
|
153
|
+
const seatOffset = getBoatPassengerSeatOffset(passengerIndex, passengerCount)
|
|
154
|
+
return {
|
|
155
|
+
x: boatWorldPos.x - Math.sin(boatYaw) * seatOffset,
|
|
156
|
+
y: boatWorldPos.y + BOAT_PASSENGER_RIDING_OFFSET_Y + PLAYER_RIDING_OFFSET_Y,
|
|
157
|
+
z: boatWorldPos.z + Math.cos(boatYaw) * seatOffset
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Vanilla 1.17.1 minecart positionRider for a centered player passenger. */
|
|
162
|
+
export function getMinecartPassengerWorldPosition(minecartWorldPos: Vec3Like): Vec3Like {
|
|
163
|
+
return {
|
|
164
|
+
x: minecartWorldPos.x,
|
|
165
|
+
y: minecartWorldPos.y + MINECART_PASSENGER_RIDING_OFFSET_Y + PLAYER_RIDING_OFFSET_Y,
|
|
166
|
+
z: minecartWorldPos.z
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getHorsePassengerFeetOffsetY(name: string | undefined, height: number): number {
|
|
171
|
+
let variantOffset = 0
|
|
172
|
+
if (name === 'donkey' || name === 'mule') {
|
|
173
|
+
variantOffset = 0.25
|
|
174
|
+
} else if (name === 'skeleton_horse') {
|
|
175
|
+
variantOffset = 0.1875
|
|
176
|
+
}
|
|
177
|
+
return height * 0.75 - variantOffset - 0.35
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Vanilla 1.17.1 AbstractHorse positionRider feet Y for a centered player passenger. */
|
|
181
|
+
export function getHorsePassengerWorldPosition(vehicleWorldPos: Vec3Like, name: string | undefined, height = 1.6): Vec3Like {
|
|
182
|
+
return {
|
|
183
|
+
x: vehicleWorldPos.x,
|
|
184
|
+
y: vehicleWorldPos.y + getHorsePassengerFeetOffsetY(name, height),
|
|
185
|
+
z: vehicleWorldPos.z
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import { applyLocalHorseCameraYawLock, shouldApplyLocalHorseCameraYawLock, type EntityRenderHints } from './interpolationPolicy'
|
|
5
|
+
|
|
6
|
+
function makeHorseSceneEntity(initialYaw = 0) {
|
|
7
|
+
const entity = new THREE.Group()
|
|
8
|
+
entity.rotation.y = initialYaw
|
|
9
|
+
entity.userData = {
|
|
10
|
+
_horseBodyYaw: initialYaw,
|
|
11
|
+
_horseHeadYaw: initialYaw
|
|
12
|
+
}
|
|
13
|
+
return entity
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('local controlled horse yaw equals camera yaw without entityMoved', () => {
|
|
17
|
+
const entity = makeHorseSceneEntity(1.2)
|
|
18
|
+
const cameraYaw = 2.5
|
|
19
|
+
|
|
20
|
+
expect(applyLocalHorseCameraYawLock(entity, cameraYaw)).toBe(true)
|
|
21
|
+
expect(entity.rotation.y).toBe(cameraYaw)
|
|
22
|
+
expect(entity.userData._horseBodyYaw).toBe(cameraYaw)
|
|
23
|
+
expect(entity.userData._horseHeadYaw).toBe(cameraYaw)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('two sequential camera yaw changes immediately update the model', () => {
|
|
27
|
+
const entity = makeHorseSceneEntity(0)
|
|
28
|
+
|
|
29
|
+
applyLocalHorseCameraYawLock(entity, 1.0)
|
|
30
|
+
expect(entity.rotation.y).toBe(1.0)
|
|
31
|
+
|
|
32
|
+
applyLocalHorseCameraYawLock(entity, 1.5)
|
|
33
|
+
expect(entity.rotation.y).toBe(1.5)
|
|
34
|
+
expect(entity.userData._horseBodyYaw).toBe(1.5)
|
|
35
|
+
expect(entity.userData._horseHeadYaw).toBe(1.5)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('wrap ±π assigns camera yaw directly without shortest-path adjustment', () => {
|
|
39
|
+
const entity = makeHorseSceneEntity(Math.PI - 0.01)
|
|
40
|
+
const cameraYaw = -Math.PI + 0.01
|
|
41
|
+
|
|
42
|
+
applyLocalHorseCameraYawLock(entity, cameraYaw)
|
|
43
|
+
|
|
44
|
+
expect(entity.rotation.y).toBeCloseTo(cameraYaw, 5)
|
|
45
|
+
expect(entity.userData._horseBodyYaw).toBeCloseTo(cameraYaw, 5)
|
|
46
|
+
expect(entity.userData._horseHeadYaw).toBeCloseTo(cameraYaw, 5)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('non-finite camera yaw is ignored', () => {
|
|
50
|
+
const entity = makeHorseSceneEntity(1.25)
|
|
51
|
+
|
|
52
|
+
expect(applyLocalHorseCameraYawLock(entity, Number.NaN)).toBe(false)
|
|
53
|
+
expect(applyLocalHorseCameraYawLock(entity, Number.POSITIVE_INFINITY)).toBe(false)
|
|
54
|
+
expect(entity.rotation.y).toBe(1.25)
|
|
55
|
+
expect(entity.userData._horseBodyYaw).toBe(1.25)
|
|
56
|
+
expect(entity.userData._horseHeadYaw).toBe(1.25)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('shouldApplyLocalHorseCameraYawLock is true only for local horse yaw lock hint', () => {
|
|
60
|
+
expect(shouldApplyLocalHorseCameraYawLock({ localVehicleYawLock: 'horse' })).toBe(true)
|
|
61
|
+
expect(shouldApplyLocalHorseCameraYawLock({ localVehicle: true, passengerLayout: 'horse' })).toBe(false)
|
|
62
|
+
expect(shouldApplyLocalHorseCameraYawLock({ localVehicle: true, localVehicleVerticalCameraLock: 'horse' })).toBe(false)
|
|
63
|
+
expect(shouldApplyLocalHorseCameraYawLock(undefined)).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test.each([
|
|
67
|
+
['remote horse', { passengerLayout: 'horse' } satisfies EntityRenderHints],
|
|
68
|
+
['local boat', { localVehicle: true, passengerLayout: 'boat' } satisfies EntityRenderHints],
|
|
69
|
+
['local minecart', { localVehicle: true, passengerLayout: 'minecart' } satisfies EntityRenderHints]
|
|
70
|
+
])('%s does not receive yaw lock', (_label, renderHints) => {
|
|
71
|
+
expect(shouldApplyLocalHorseCameraYawLock(renderHints)).toBe(false)
|
|
72
|
+
})
|