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.
- package/dist/minecraft-renderer.js +64 -64
- 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/types.ts +4 -1
- package/src/lib/worldrendererCommon.ts +2 -2
- package/src/three/entities.ts +303 -33
- 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 +281 -0
- package/src/three/entity/interpolationPolicy.ts +242 -0
- package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
- package/src/three/entity/localVehicleRendering.integration.test.ts +267 -0
- package/src/three/entity/minecartLocalPassengerRendering.test.ts +210 -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 +229 -0
- package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
- package/src/three/entity/vehiclePassengerProduction.test.ts +229 -0
- package/src/three/entity/vehiclePassengerRendering.ts +56 -0
- package/src/three/entity/vehiclePassengerUpdate.ts +91 -0
- package/src/three/graphicsBackendBase.ts +5 -5
- package/src/three/graphicsBackendOffThread.ts +2 -2
- package/src/three/worldRendererThree.ts +48 -22
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { versionToNumber } from '../../lib/utils'
|
|
3
|
+
|
|
4
|
+
export const LOCAL_MOVEMENT_TWEEN_DURATION_MS = 50
|
|
5
|
+
export const ENTITY_TWEEN_DURATION_MS = 120
|
|
6
|
+
export const SPECTATING_CAMERA_TWEEN_DURATION_MS = 150
|
|
7
|
+
export const CAMERA_POSITION_EPSILON = 1e-4
|
|
8
|
+
|
|
9
|
+
export type CameraMovementMode = 'local-player' | 'server-vehicle' | 'spectating'
|
|
10
|
+
|
|
11
|
+
export type UpdateCameraOptions = {
|
|
12
|
+
movementMode?: CameraMovementMode
|
|
13
|
+
instant?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Vec3Like = { x: number; y: number; z: number }
|
|
17
|
+
|
|
18
|
+
export type EntityRenderHints = {
|
|
19
|
+
localVehicle?: boolean
|
|
20
|
+
localVehicleVerticalCameraLock?: 'horse'
|
|
21
|
+
/** Render-only: lock locally controlled horse yaw to camera each frame. */
|
|
22
|
+
localVehicleYawLock?: 'horse'
|
|
23
|
+
boatWaterPatchVisible?: boolean
|
|
24
|
+
boatPaddleLeft?: boolean
|
|
25
|
+
boatPaddleRight?: boolean
|
|
26
|
+
passengerIds?: number[]
|
|
27
|
+
passengerLayout?: 'boat' | 'minecart' | 'horse'
|
|
28
|
+
/** @deprecated Use passengerIds */
|
|
29
|
+
boatPassengerIds?: number[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type EntityWithRenderHints = {
|
|
33
|
+
renderHints?: EntityRenderHints
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function usesCameraSyncedVehiclePosition(entity: EntityWithRenderHints | undefined): boolean {
|
|
37
|
+
return !!entity?.renderHints?.localVehicle
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function shouldApplyLocalHorseCameraYawLock(renderHints: EntityRenderHints | undefined): boolean {
|
|
41
|
+
return renderHints?.localVehicleYawLock === 'horse'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Visual-only: align local horse model yaw with camera before head pose and passengers. */
|
|
45
|
+
export function applyLocalHorseCameraYawLock(sceneEntity: { rotation: { y: number }; userData: Record<string, unknown> }, cameraYaw: number): boolean {
|
|
46
|
+
if (!Number.isFinite(cameraYaw)) return false
|
|
47
|
+
sceneEntity.rotation.y = cameraYaw
|
|
48
|
+
sceneEntity.userData._horseBodyYaw = cameraYaw
|
|
49
|
+
sceneEntity.userData._horseHeadYaw = cameraYaw
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getEntityTweenDurationMs(entity: EntityWithRenderHints | undefined, justAdded: boolean): number {
|
|
54
|
+
if (justAdded) return 0
|
|
55
|
+
if (usesCameraSyncedVehiclePosition(entity)) return 0
|
|
56
|
+
return ENTITY_TWEEN_DURATION_MS
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Rotation follows the same instant-vs-remote policy as entity position. */
|
|
60
|
+
export function getEntityRotationTweenDurationMs(entity: EntityWithRenderHints | undefined, justAdded: boolean): number {
|
|
61
|
+
return getEntityTweenDurationMs(entity, justAdded)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function samePosition(a: Vec3Like, b: Vec3Like, epsilon = CAMERA_POSITION_EPSILON): boolean {
|
|
65
|
+
return Math.abs(a.x - b.x) < epsilon && Math.abs(a.y - b.y) < epsilon && Math.abs(a.z - b.z) < epsilon
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getCameraMovementTweenDurationMs(mode: CameraMovementMode, instant = false): number {
|
|
69
|
+
if (instant) return 0
|
|
70
|
+
switch (mode) {
|
|
71
|
+
case 'spectating':
|
|
72
|
+
return SPECTATING_CAMERA_TWEEN_DURATION_MS
|
|
73
|
+
case 'server-vehicle':
|
|
74
|
+
return ENTITY_TWEEN_DURATION_MS
|
|
75
|
+
case 'local-player':
|
|
76
|
+
default:
|
|
77
|
+
return LOCAL_MOVEMENT_TWEEN_DURATION_MS
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function shouldRestartCameraPositionTween(args: {
|
|
82
|
+
target: Vec3Like
|
|
83
|
+
currentTarget: Vec3Like | null
|
|
84
|
+
movementMode: CameraMovementMode
|
|
85
|
+
previousMovementMode: CameraMovementMode | null
|
|
86
|
+
instant: boolean
|
|
87
|
+
}): boolean {
|
|
88
|
+
if (args.instant) return true
|
|
89
|
+
if (args.currentTarget == null) return true
|
|
90
|
+
if (args.movementMode !== args.previousMovementMode) return true
|
|
91
|
+
return !samePosition(args.target, args.currentTarget)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Locally ridden vehicle X/Z follow camera tween; Y uses latest server vehicle height. */
|
|
95
|
+
export function getLocalVehicleWorldPosition(cameraWorldPos: Vec3Like, vehicleY: number): Vec3Like {
|
|
96
|
+
return {
|
|
97
|
+
x: cameraWorldPos.x,
|
|
98
|
+
y: vehicleY,
|
|
99
|
+
z: cameraWorldPos.z
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveLocalVehicleWorldPosition(args: {
|
|
104
|
+
cameraWorldPos: Vec3Like
|
|
105
|
+
rawVehicleY: number
|
|
106
|
+
eyeHeight: number
|
|
107
|
+
vehicleName: string | undefined
|
|
108
|
+
vehicleHeight: number
|
|
109
|
+
verticalCameraLock?: 'horse'
|
|
110
|
+
}): Vec3Like {
|
|
111
|
+
const { cameraWorldPos, rawVehicleY, eyeHeight, vehicleName, vehicleHeight, verticalCameraLock } = args
|
|
112
|
+
if (verticalCameraLock === 'horse') {
|
|
113
|
+
const y = cameraWorldPos.y - eyeHeight - getHorsePassengerFeetOffsetY(vehicleName, vehicleHeight)
|
|
114
|
+
if (Number.isFinite(y)) {
|
|
115
|
+
return { x: cameraWorldPos.x, y, z: cameraWorldPos.z }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return getLocalVehicleWorldPosition(cameraWorldPos, rawVehicleY)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const RIDEABLE_MINECART_ENTITY_NAMES = new Set([
|
|
122
|
+
'minecart',
|
|
123
|
+
'chest_minecart',
|
|
124
|
+
'furnace_minecart',
|
|
125
|
+
'hopper_minecart',
|
|
126
|
+
'tnt_minecart',
|
|
127
|
+
'spawner_minecart',
|
|
128
|
+
'command_block_minecart'
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
const RIDEABLE_HORSE_ENTITY_NAMES = new Set(['horse', 'donkey', 'mule', 'skeleton_horse', 'zombie_horse'])
|
|
132
|
+
|
|
133
|
+
export function isRideableHorseEntityName(name?: string): boolean {
|
|
134
|
+
if (!name) return false
|
|
135
|
+
return RIDEABLE_HORSE_ENTITY_NAMES.has(name)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isRideableMinecartEntityName(name?: string): boolean {
|
|
139
|
+
if (!name) return false
|
|
140
|
+
return RIDEABLE_MINECART_ENTITY_NAMES.has(name)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getBoatPassengerSeatOffset(passengerIndex: number, passengerCount: number): number {
|
|
144
|
+
if (passengerCount <= 1) return 0
|
|
145
|
+
return passengerIndex === 0 ? 0.2 : -0.6
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const DEFAULT_BOAT_HEIGHT = 0.5625
|
|
149
|
+
const DEFAULT_MINECART_HEIGHT = 0.7
|
|
150
|
+
|
|
151
|
+
// <= 1.20.1: Entity#positionRider = y + vehicle.getPassengersRidingOffset() + passenger.getMyRidingOffset()
|
|
152
|
+
// Vanilla 1.17.1 sources: vehicle/Boat#getPassengersRidingOffset() = -0.1,
|
|
153
|
+
// vehicle/AbstractMinecart#getPassengersRidingOffset() = 0, player/Player#getMyRidingOffset() = -0.35.
|
|
154
|
+
const LEGACY_PLAYER_RIDING_OFFSET_Y = -0.35 // Player#getMyRidingOffset(), <= 1.20.1
|
|
155
|
+
const LEGACY_BOAT_RIDING_OFFSET_Y = -0.1 // Boat#getPassengersRidingOffset(), <= 1.20.1
|
|
156
|
+
const LEGACY_MINECART_RIDING_OFFSET_Y = 0 // AbstractMinecart#getPassengersRidingOffset(), <= 1.20.1
|
|
157
|
+
|
|
158
|
+
// >= 1.20.2: y is based on the vehicle's PASSENGER attachment point.
|
|
159
|
+
// The player's contribution is numerically the same, but the mechanism changed:
|
|
160
|
+
// 1.20.2-1.20.4: PASSENGER.y + Player#ridingOffset() (-0.6)
|
|
161
|
+
// >= 1.20.5: PASSENGER.y - Player.DEFAULT_VEHICLE_ATTACHMENT.y (0.6)
|
|
162
|
+
// Vanilla sources: Entity#positionRider, Boat#getPassengerAttachmentPoint,
|
|
163
|
+
// EntityType.*_MINECART#passengerAttachments and Player#ridingOffset / DEFAULT_VEHICLE_ATTACHMENT.
|
|
164
|
+
const MODERN_PLAYER_RIDING_OFFSET_Y = -0.6
|
|
165
|
+
const MODERN_MINECART_PASSENGER_ATTACHMENT_Y = 0.1875 // EntityType.*_MINECART.passengerAttachments()
|
|
166
|
+
const MODERN_BOAT_RIDE_HEIGHT_FACTOR = 1 / 3 // (Abstract)Boat#rideHeight(), >= 1.20.2
|
|
167
|
+
const MODERN_RAFT_RIDE_HEIGHT_FACTOR = 0.8888889 // Raft#rideHeight(), >= 1.21.2
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Vanilla seat offset for a player passenger in a boat or minecart.
|
|
171
|
+
* Rafts before 1.21.2 are intentionally treated as boats because their entity
|
|
172
|
+
* name is 'boat' and their metadata variant is not read by this project.
|
|
173
|
+
*/
|
|
174
|
+
export function getVehiclePassengerFeetOffsetY(
|
|
175
|
+
layout: 'boat' | 'minecart',
|
|
176
|
+
version: string,
|
|
177
|
+
vehicleName: string | undefined,
|
|
178
|
+
vehicleHeight: number | undefined
|
|
179
|
+
): number {
|
|
180
|
+
const height = Number.isFinite(vehicleHeight) ? vehicleHeight! : layout === 'boat' ? DEFAULT_BOAT_HEIGHT : DEFAULT_MINECART_HEIGHT
|
|
181
|
+
|
|
182
|
+
if (versionToNumber(version) >= versionToNumber('1.20.2')) {
|
|
183
|
+
const isRaft = typeof vehicleName === 'string' && vehicleName.toLowerCase().endsWith('_raft')
|
|
184
|
+
const passengerAttachmentY =
|
|
185
|
+
layout === 'boat' ? height * (isRaft ? MODERN_RAFT_RIDE_HEIGHT_FACTOR : MODERN_BOAT_RIDE_HEIGHT_FACTOR) : MODERN_MINECART_PASSENGER_ATTACHMENT_Y
|
|
186
|
+
return passengerAttachmentY + MODERN_PLAYER_RIDING_OFFSET_Y
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const vehicleRidingOffset = layout === 'boat' ? LEGACY_BOAT_RIDING_OFFSET_Y : LEGACY_MINECART_RIDING_OFFSET_Y
|
|
190
|
+
return vehicleRidingOffset + LEGACY_PLAYER_RIDING_OFFSET_Y
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Vanilla Boat#positionRider position, without applying the passenger pose. */
|
|
194
|
+
export function getBoatPassengerWorldPosition(
|
|
195
|
+
boatWorldPos: Vec3Like,
|
|
196
|
+
boatYaw: number,
|
|
197
|
+
passengerIndex: number,
|
|
198
|
+
passengerCount: number,
|
|
199
|
+
version: string,
|
|
200
|
+
vehicleName: string | undefined,
|
|
201
|
+
vehicleHeight: number | undefined
|
|
202
|
+
): Vec3Like {
|
|
203
|
+
const seatOffset = getBoatPassengerSeatOffset(passengerIndex, passengerCount)
|
|
204
|
+
return {
|
|
205
|
+
x: boatWorldPos.x - Math.sin(boatYaw) * seatOffset,
|
|
206
|
+
y: boatWorldPos.y + getVehiclePassengerFeetOffsetY('boat', version, vehicleName, vehicleHeight),
|
|
207
|
+
z: boatWorldPos.z + Math.cos(boatYaw) * seatOffset
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Vanilla minecart positionRider for a centered player passenger. */
|
|
212
|
+
export function getMinecartPassengerWorldPosition(
|
|
213
|
+
minecartWorldPos: Vec3Like,
|
|
214
|
+
version: string,
|
|
215
|
+
vehicleName: string | undefined,
|
|
216
|
+
vehicleHeight: number | undefined
|
|
217
|
+
): Vec3Like {
|
|
218
|
+
return {
|
|
219
|
+
x: minecartWorldPos.x,
|
|
220
|
+
y: minecartWorldPos.y + getVehiclePassengerFeetOffsetY('minecart', version, vehicleName, vehicleHeight),
|
|
221
|
+
z: minecartWorldPos.z
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getHorsePassengerFeetOffsetY(name: string | undefined, height: number): number {
|
|
226
|
+
let variantOffset = 0
|
|
227
|
+
if (name === 'donkey' || name === 'mule') {
|
|
228
|
+
variantOffset = 0.25
|
|
229
|
+
} else if (name === 'skeleton_horse') {
|
|
230
|
+
variantOffset = 0.1875
|
|
231
|
+
}
|
|
232
|
+
return height * 0.75 - variantOffset - 0.35
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Vanilla 1.17.1 AbstractHorse positionRider feet Y for a centered player passenger. */
|
|
236
|
+
export function getHorsePassengerWorldPosition(vehicleWorldPos: Vec3Like, name: string | undefined, height = 1.6): Vec3Like {
|
|
237
|
+
return {
|
|
238
|
+
x: vehicleWorldPos.x,
|
|
239
|
+
y: vehicleWorldPos.y + getHorsePassengerFeetOffsetY(name, height),
|
|
240
|
+
z: vehicleWorldPos.z
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { expect, test, vi } from 'vitest'
|
|
4
|
+
import { SceneOrigin } from '../sceneOrigin'
|
|
5
|
+
import { anchorVehiclePassengerPosition, releaseVehiclePassengerPosition } from './vehiclePassengerRendering'
|
|
6
|
+
import {
|
|
7
|
+
ENTITY_TWEEN_DURATION_MS,
|
|
8
|
+
getBoatPassengerWorldPosition,
|
|
9
|
+
getEntityTweenDurationMs,
|
|
10
|
+
getLocalVehicleWorldPosition,
|
|
11
|
+
getMinecartPassengerWorldPosition,
|
|
12
|
+
usesCameraSyncedVehiclePosition
|
|
13
|
+
} from './interpolationPolicy'
|
|
14
|
+
|
|
15
|
+
type Vec3 = { x: number; y: number; z: number }
|
|
16
|
+
|
|
17
|
+
const TEST_VERSION = '1.17.1'
|
|
18
|
+
const BOAT_HEIGHT = 0.5625
|
|
19
|
+
const MINECART_HEIGHT = 0.7
|
|
20
|
+
|
|
21
|
+
function staleOffsetVehiclePosition(cameraWorldPos: Vec3, vehiclePosition: Vec3, stalePassengerPosition: Vec3): Vec3 {
|
|
22
|
+
return {
|
|
23
|
+
x: cameraWorldPos.x + (vehiclePosition.x - stalePassengerPosition.x),
|
|
24
|
+
y: cameraWorldPos.y + (vehiclePosition.y - stalePassengerPosition.y),
|
|
25
|
+
z: cameraWorldPos.z + (vehiclePosition.z - stalePassengerPosition.z)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('stale passenger snapshot does not shift rendered locally ridden vehicle X/Z relative to camera', () => {
|
|
30
|
+
const stalePassenger = { x: 0, y: 63, z: 0 }
|
|
31
|
+
const newVehicle = { x: 1, y: 63.2, z: 2 }
|
|
32
|
+
const cameraWorldPos = { x: 0.4, y: 63.5, z: 0.8 }
|
|
33
|
+
|
|
34
|
+
// 1. Vehicle position updates before passenger sync.
|
|
35
|
+
// 2. Renderer receives vehicle snapshot while passenger is still stale.
|
|
36
|
+
const broken = staleOffsetVehiclePosition(cameraWorldPos, newVehicle, stalePassenger)
|
|
37
|
+
const rendered = getLocalVehicleWorldPosition(cameraWorldPos, newVehicle.y)
|
|
38
|
+
|
|
39
|
+
expect(rendered.x).toBe(cameraWorldPos.x)
|
|
40
|
+
expect(rendered.z).toBe(cameraWorldPos.z)
|
|
41
|
+
expect(rendered.y).toBe(newVehicle.y)
|
|
42
|
+
expect(broken.x).not.toBe(cameraWorldPos.x)
|
|
43
|
+
expect(broken.z).not.toBe(cameraWorldPos.z)
|
|
44
|
+
|
|
45
|
+
// 3. Passenger synchronizes to vehicle.
|
|
46
|
+
const syncedPassenger = { ...newVehicle, y: 63 }
|
|
47
|
+
const syncedCamera = { x: 0.8, y: 63.5, z: 1.6 }
|
|
48
|
+
|
|
49
|
+
// 4. Boat X/Z remain fixed relative to the player/camera on intermediate frames.
|
|
50
|
+
const afterSync = getLocalVehicleWorldPosition(syncedCamera, newVehicle.y)
|
|
51
|
+
expect(afterSync.x - syncedCamera.x).toBe(0)
|
|
52
|
+
expect(afterSync.z - syncedCamera.z).toBe(0)
|
|
53
|
+
expect(afterSync.y).toBe(newVehicle.y)
|
|
54
|
+
expect(syncedPassenger.x).toBe(newVehicle.x)
|
|
55
|
+
expect(syncedPassenger.z).toBe(newVehicle.z)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('render frames between press and release keep zero horizontal delta to camera for local boat', () => {
|
|
59
|
+
const vehicleY = 62.75
|
|
60
|
+
const frames = [
|
|
61
|
+
{ x: 0, y: 63.4, z: 0 },
|
|
62
|
+
{ x: 0.2, y: 63.4, z: 0 },
|
|
63
|
+
{ x: 0.45, y: 63.4, z: 0 },
|
|
64
|
+
{ x: 0.7, y: 63.4, z: 0 },
|
|
65
|
+
{ x: 0.7, y: 63.4, z: 0 },
|
|
66
|
+
{ x: 0.55, y: 63.4, z: 0 }
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for (const camera of frames) {
|
|
70
|
+
const boat = getLocalVehicleWorldPosition(camera, vehicleY)
|
|
71
|
+
expect(boat.x - camera.x).toBe(0)
|
|
72
|
+
expect(boat.z - camera.z).toBe(0)
|
|
73
|
+
expect(boat.y).toBe(vehicleY)
|
|
74
|
+
expect(boat.y).not.toBe(camera.y)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('boat passenger positions use vanilla 1.17.1 riding and seat offsets', () => {
|
|
79
|
+
const boat = { x: 10, y: 64, z: 20 }
|
|
80
|
+
|
|
81
|
+
expect(getBoatPassengerWorldPosition(boat, 0, 0, 1, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)).toEqual({
|
|
82
|
+
x: 10,
|
|
83
|
+
y: 63.55,
|
|
84
|
+
z: 20
|
|
85
|
+
})
|
|
86
|
+
expect(getBoatPassengerWorldPosition(boat, 0, 0, 2, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)).toEqual({
|
|
87
|
+
x: 10,
|
|
88
|
+
y: 63.55,
|
|
89
|
+
z: 20.2
|
|
90
|
+
})
|
|
91
|
+
expect(getBoatPassengerWorldPosition(boat, 0, 1, 2, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)).toEqual({
|
|
92
|
+
x: 10,
|
|
93
|
+
y: 63.55,
|
|
94
|
+
z: 19.4
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const rotated = getBoatPassengerWorldPosition(boat, Math.PI / 2, 0, 2, TEST_VERSION, 'oak_boat', BOAT_HEIGHT)
|
|
98
|
+
expect(rotated.x).toBeCloseTo(9.8)
|
|
99
|
+
expect(rotated.y).toBe(63.55)
|
|
100
|
+
expect(rotated.z).toBeCloseTo(20)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('remote player follows the tracked boat each frame and releases from an empty passenger list', () => {
|
|
104
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
105
|
+
sceneOrigin.update(96, 60, 192)
|
|
106
|
+
|
|
107
|
+
const boat = new THREE.Group()
|
|
108
|
+
sceneOrigin.track(boat)
|
|
109
|
+
boat.position.set(100, 64, 200)
|
|
110
|
+
|
|
111
|
+
const passenger = new THREE.Group()
|
|
112
|
+
const stopPositionTween = vi.fn()
|
|
113
|
+
passenger.userData._posTween = { stop: stopPositionTween }
|
|
114
|
+
sceneOrigin.track(passenger)
|
|
115
|
+
passenger.position.set(90, 64, 190)
|
|
116
|
+
|
|
117
|
+
const firstPassengerPosition = getBoatPassengerWorldPosition(
|
|
118
|
+
sceneOrigin.getWorldPosition(boat)!,
|
|
119
|
+
boat.rotation.y,
|
|
120
|
+
0,
|
|
121
|
+
1,
|
|
122
|
+
TEST_VERSION,
|
|
123
|
+
'oak_boat',
|
|
124
|
+
BOAT_HEIGHT
|
|
125
|
+
)
|
|
126
|
+
anchorVehiclePassengerPosition(passenger, firstPassengerPosition, '10')
|
|
127
|
+
|
|
128
|
+
expect(stopPositionTween).toHaveBeenCalledOnce()
|
|
129
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual({ x: 100, y: 63.55, z: 200 })
|
|
130
|
+
expect(passenger.userData._tweenTarget).toEqual({ x: 100, y: 63.55, z: 200 })
|
|
131
|
+
|
|
132
|
+
boat.position.set(102, 64.2, 203)
|
|
133
|
+
boat.rotation.y = Math.PI / 2
|
|
134
|
+
const movedPassengerPosition = getBoatPassengerWorldPosition(
|
|
135
|
+
sceneOrigin.getWorldPosition(boat)!,
|
|
136
|
+
boat.rotation.y,
|
|
137
|
+
0,
|
|
138
|
+
1,
|
|
139
|
+
TEST_VERSION,
|
|
140
|
+
'oak_boat',
|
|
141
|
+
BOAT_HEIGHT
|
|
142
|
+
)
|
|
143
|
+
anchorVehiclePassengerPosition(passenger, movedPassengerPosition, '10')
|
|
144
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.x).toBe(102)
|
|
145
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.y).toBeCloseTo(63.75)
|
|
146
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.z).toBe(203)
|
|
147
|
+
|
|
148
|
+
sceneOrigin.update(112, 70, 208)
|
|
149
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.x).toBe(102)
|
|
150
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.y).toBeCloseTo(63.75)
|
|
151
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.z).toBe(203)
|
|
152
|
+
|
|
153
|
+
const detachedWorldPosition = sceneOrigin.getWorldPosition(passenger)
|
|
154
|
+
expect(releaseVehiclePassengerPosition(passenger, '10', detachedWorldPosition)).toBe(true)
|
|
155
|
+
expect(passenger.userData._passengerVehicleId).toBeUndefined()
|
|
156
|
+
expect(passenger.userData._boatPassengerVehicleId).toBeUndefined()
|
|
157
|
+
expect(passenger.userData._tweenTarget).toEqual(detachedWorldPosition)
|
|
158
|
+
|
|
159
|
+
boat.position.set(110, 65, 210)
|
|
160
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual(detachedWorldPosition)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('minecart passenger uses vanilla 1.17.1 riding Y offset', () => {
|
|
164
|
+
const minecart = { x: 10, y: 64, z: 20 }
|
|
165
|
+
expect(getMinecartPassengerWorldPosition(minecart, TEST_VERSION, 'minecart', MINECART_HEIGHT)).toEqual({
|
|
166
|
+
x: 10,
|
|
167
|
+
y: 63.65,
|
|
168
|
+
z: 20
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('minecart passenger anchor follows vehicle world position without inheriting vehicle yaw', () => {
|
|
173
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
174
|
+
sceneOrigin.update(96, 60, 192)
|
|
175
|
+
|
|
176
|
+
const minecart = new THREE.Group()
|
|
177
|
+
sceneOrigin.track(minecart)
|
|
178
|
+
minecart.position.set(100, 64, 200)
|
|
179
|
+
minecart.rotation.y = Math.PI / 2
|
|
180
|
+
|
|
181
|
+
const passenger = new THREE.Group()
|
|
182
|
+
passenger.rotation.y = 1.25
|
|
183
|
+
sceneOrigin.track(passenger)
|
|
184
|
+
passenger.position.set(90, 64, 190)
|
|
185
|
+
|
|
186
|
+
const firstPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!, TEST_VERSION, 'minecart', MINECART_HEIGHT)
|
|
187
|
+
anchorVehiclePassengerPosition(passenger, firstPosition, '42')
|
|
188
|
+
const anchoredYaw = passenger.rotation.y
|
|
189
|
+
|
|
190
|
+
minecart.position.set(103, 64.5, 205)
|
|
191
|
+
minecart.rotation.y = Math.PI
|
|
192
|
+
const movedPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!, TEST_VERSION, 'minecart', MINECART_HEIGHT)
|
|
193
|
+
anchorVehiclePassengerPosition(passenger, movedPosition, '42')
|
|
194
|
+
|
|
195
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual({
|
|
196
|
+
x: 103,
|
|
197
|
+
y: 64.15,
|
|
198
|
+
z: 205
|
|
199
|
+
})
|
|
200
|
+
expect(passenger.rotation.y).toBe(anchoredYaw)
|
|
201
|
+
|
|
202
|
+
const detachedWorldPosition = sceneOrigin.getWorldPosition(passenger)
|
|
203
|
+
expect(releaseVehiclePassengerPosition(passenger, '42', detachedWorldPosition)).toBe(true)
|
|
204
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual(detachedWorldPosition)
|
|
205
|
+
|
|
206
|
+
minecart.position.set(120, 66, 220)
|
|
207
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual(detachedWorldPosition)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('local minecart with localVehicle uses camera-synced positioning', () => {
|
|
211
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { localVehicle: true, passengerLayout: 'minecart' } })).toBe(true)
|
|
212
|
+
expect(getEntityTweenDurationMs({ renderHints: { localVehicle: true, passengerLayout: 'minecart' } }, false)).toBe(0)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('remote minecart keeps ordinary entity interpolation', () => {
|
|
216
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { passengerLayout: 'minecart' } })).toBe(false)
|
|
217
|
+
expect(getEntityTweenDurationMs({ renderHints: { passengerLayout: 'minecart' } }, false)).toBe(ENTITY_TWEEN_DURATION_MS)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('local minecart X/Z stay aligned with camera on intermediate frames between server targets', () => {
|
|
221
|
+
const vehicleY = 63.7
|
|
222
|
+
const cameraFrames = [
|
|
223
|
+
{ x: 0, y: 64.3, z: 0 },
|
|
224
|
+
{ x: 1.2, y: 64.3, z: 0.6 },
|
|
225
|
+
{ x: 5, y: 64.3, z: 3 },
|
|
226
|
+
{ x: 4.1, y: 64.3, z: 2.5 }
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
for (const camera of cameraFrames) {
|
|
230
|
+
const minecart = getLocalVehicleWorldPosition(camera, vehicleY)
|
|
231
|
+
expect(minecart.x - camera.x).toBeCloseTo(0, 5)
|
|
232
|
+
expect(minecart.z - camera.z).toBeCloseTo(0, 5)
|
|
233
|
+
expect(minecart.y).toBe(vehicleY)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('local minecart passenger anchors to camera-synced vehicle position', () => {
|
|
238
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
239
|
+
sceneOrigin.update(96, 60, 192)
|
|
240
|
+
|
|
241
|
+
const minecart = new THREE.Group()
|
|
242
|
+
sceneOrigin.track(minecart)
|
|
243
|
+
|
|
244
|
+
const passenger = new THREE.Group()
|
|
245
|
+
sceneOrigin.track(passenger)
|
|
246
|
+
|
|
247
|
+
const vehicleY = 64
|
|
248
|
+
const cameraFrames = [
|
|
249
|
+
{ x: 100, y: 64.7, z: 200 },
|
|
250
|
+
{ x: 101.4, y: 64.7, z: 200.8 },
|
|
251
|
+
{ x: 103, y: 64.7, z: 202 }
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
for (const camera of cameraFrames) {
|
|
255
|
+
const syncedMinecart = getLocalVehicleWorldPosition(camera, vehicleY)
|
|
256
|
+
minecart.position.set(syncedMinecart.x, syncedMinecart.y, syncedMinecart.z)
|
|
257
|
+
const seatPosition = getMinecartPassengerWorldPosition(syncedMinecart, TEST_VERSION, 'minecart', MINECART_HEIGHT)
|
|
258
|
+
anchorVehiclePassengerPosition(passenger, seatPosition, '42')
|
|
259
|
+
const passengerWorld = sceneOrigin.getWorldPosition(passenger)!
|
|
260
|
+
|
|
261
|
+
expect(syncedMinecart.x - camera.x).toBeCloseTo(0, 5)
|
|
262
|
+
expect(syncedMinecart.z - camera.z).toBeCloseTo(0, 5)
|
|
263
|
+
expect(passengerWorld.x).toBe(syncedMinecart.x)
|
|
264
|
+
expect(passengerWorld.z).toBe(syncedMinecart.z)
|
|
265
|
+
expect(passengerWorld.y).toBeCloseTo(63.65)
|
|
266
|
+
}
|
|
267
|
+
})
|