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,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
|