minecraft-renderer 0.1.93 → 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 +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 +302 -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 +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 +48 -22
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import { SceneOrigin } from '../sceneOrigin'
|
|
5
|
+
import { anchorVehiclePassengerPosition, releaseVehiclePassengerPosition } from './vehiclePassengerRendering'
|
|
6
|
+
import { getBoatPassengerWorldPosition, getMinecartPassengerWorldPosition } from './interpolationPolicy'
|
|
7
|
+
|
|
8
|
+
const LOCAL_PLAYER_ID = 7
|
|
9
|
+
const REMOTE_PLAYER_ID = 8
|
|
10
|
+
const MINECART_ID = '42'
|
|
11
|
+
const EYE_HEIGHT = 1.62
|
|
12
|
+
const PLAYER_HEIGHT = 1.8
|
|
13
|
+
|
|
14
|
+
type PassengerLike = THREE.Group & {
|
|
15
|
+
playerObject: Record<string, unknown>
|
|
16
|
+
originalEntity: { id: number }
|
|
17
|
+
userData: Record<string, unknown>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makePassenger(id: number): PassengerLike {
|
|
21
|
+
const passenger = new THREE.Group() as PassengerLike
|
|
22
|
+
passenger.playerObject = {}
|
|
23
|
+
passenger.originalEntity = { id }
|
|
24
|
+
passenger.userData = {}
|
|
25
|
+
return passenger
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveMinecartPassenger(
|
|
29
|
+
passengerId: number,
|
|
30
|
+
localPlayer: PassengerLike | null,
|
|
31
|
+
remotePlayers: Record<number, PassengerLike>
|
|
32
|
+
): PassengerLike | undefined {
|
|
33
|
+
const isLocalPassenger = passengerId === localPlayer?.originalEntity.id
|
|
34
|
+
return isLocalPassenger ? (localPlayer ?? undefined) : remotePlayers[passengerId]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function anchorMinecartPassengers(
|
|
38
|
+
minecart: THREE.Group,
|
|
39
|
+
sceneOrigin: SceneOrigin,
|
|
40
|
+
passengerIds: number[],
|
|
41
|
+
localPlayer: PassengerLike | null,
|
|
42
|
+
remotePlayers: Record<number, PassengerLike>
|
|
43
|
+
) {
|
|
44
|
+
const vehicleWorldPos = sceneOrigin.getWorldPosition(minecart)!
|
|
45
|
+
const seatPosition = getMinecartPassengerWorldPosition(vehicleWorldPos)
|
|
46
|
+
for (const passengerId of passengerIds) {
|
|
47
|
+
const passenger = resolveMinecartPassenger(passengerId, localPlayer, remotePlayers)
|
|
48
|
+
if (!passenger?.playerObject) continue
|
|
49
|
+
anchorVehiclePassengerPosition(passenger, seatPosition, MINECART_ID)
|
|
50
|
+
}
|
|
51
|
+
return seatPosition
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('local and remote minecart passengers share the same seat Y', () => {
|
|
55
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
56
|
+
sceneOrigin.update(96, 60, 192)
|
|
57
|
+
|
|
58
|
+
const minecart = new THREE.Group()
|
|
59
|
+
sceneOrigin.track(minecart)
|
|
60
|
+
minecart.position.set(100, 64, 200)
|
|
61
|
+
|
|
62
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
63
|
+
const remotePlayer = makePassenger(REMOTE_PLAYER_ID)
|
|
64
|
+
sceneOrigin.track(localPlayer)
|
|
65
|
+
sceneOrigin.track(remotePlayer)
|
|
66
|
+
|
|
67
|
+
const seatPosition = anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID, REMOTE_PLAYER_ID], localPlayer, { [REMOTE_PLAYER_ID]: remotePlayer })
|
|
68
|
+
|
|
69
|
+
const localWorld = sceneOrigin.getWorldPosition(localPlayer)
|
|
70
|
+
const remoteWorld = sceneOrigin.getWorldPosition(remotePlayer)
|
|
71
|
+
expect(localWorld?.y).toBe(seatPosition.y)
|
|
72
|
+
expect(remoteWorld?.y).toBe(seatPosition.y)
|
|
73
|
+
expect(localWorld).toEqual(remoteWorld)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('local third-person player follows interpolated minecart world position', () => {
|
|
77
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
78
|
+
sceneOrigin.update(96, 60, 192)
|
|
79
|
+
|
|
80
|
+
const minecart = new THREE.Group()
|
|
81
|
+
sceneOrigin.track(minecart)
|
|
82
|
+
minecart.position.set(100, 64, 200)
|
|
83
|
+
|
|
84
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
85
|
+
sceneOrigin.track(localPlayer)
|
|
86
|
+
|
|
87
|
+
anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
88
|
+
|
|
89
|
+
minecart.position.set(103, 64.5, 205)
|
|
90
|
+
sceneOrigin.update(112, 70, 208)
|
|
91
|
+
const movedSeat = anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
92
|
+
|
|
93
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(movedSeat)
|
|
94
|
+
expect(movedSeat.x).toBe(103)
|
|
95
|
+
expect(movedSeat.y).toBeCloseTo(64.15)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('minecart seat places player waist-up inside the cart instead of on camera feet', () => {
|
|
99
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
100
|
+
sceneOrigin.update(96, 60, 192)
|
|
101
|
+
|
|
102
|
+
const minecart = new THREE.Group()
|
|
103
|
+
sceneOrigin.track(minecart)
|
|
104
|
+
minecart.position.set(100, 64, 200)
|
|
105
|
+
|
|
106
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
107
|
+
sceneOrigin.track(localPlayer)
|
|
108
|
+
|
|
109
|
+
const cameraWorldPos = { x: 100, y: 64 + 0.7 + EYE_HEIGHT, z: 200 }
|
|
110
|
+
localPlayer.position.set(cameraWorldPos.x, cameraWorldPos.y - EYE_HEIGHT, cameraWorldPos.z)
|
|
111
|
+
|
|
112
|
+
const seatPosition = anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
113
|
+
const anchoredWorld = sceneOrigin.getWorldPosition(localPlayer)!
|
|
114
|
+
|
|
115
|
+
const minecartWorld = sceneOrigin.getWorldPosition(minecart)!
|
|
116
|
+
const cameraFeetY = cameraWorldPos.y - EYE_HEIGHT
|
|
117
|
+
|
|
118
|
+
expect(anchoredWorld.y).toBeLessThan(cameraFeetY)
|
|
119
|
+
expect(anchoredWorld.y).toBe(seatPosition.y)
|
|
120
|
+
const waistY = anchoredWorld.y + PLAYER_HEIGHT * 0.5
|
|
121
|
+
expect(waistY).toBeGreaterThan(minecartWorld.y)
|
|
122
|
+
expect(waistY).toBeGreaterThan(minecartWorld.y + 0.3)
|
|
123
|
+
expect(waistY).toBeLessThan(minecartWorld.y + 0.85)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('first and third person keep the same minecart passenger anchor', () => {
|
|
127
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
128
|
+
sceneOrigin.update(96, 60, 192)
|
|
129
|
+
|
|
130
|
+
const minecart = new THREE.Group()
|
|
131
|
+
sceneOrigin.track(minecart)
|
|
132
|
+
minecart.position.set(100, 64, 200)
|
|
133
|
+
|
|
134
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
135
|
+
sceneOrigin.track(localPlayer)
|
|
136
|
+
|
|
137
|
+
anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
138
|
+
const thirdPersonAnchor = { ...sceneOrigin.getWorldPosition(localPlayer)! }
|
|
139
|
+
|
|
140
|
+
localPlayer.visible = false
|
|
141
|
+
anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
142
|
+
|
|
143
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(thirdPersonAnchor)
|
|
144
|
+
expect(localPlayer.userData._passengerVehicleId).toBe(MINECART_ID)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('minecart dismount releases local player anchor without snapping back', () => {
|
|
148
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
149
|
+
sceneOrigin.update(96, 60, 192)
|
|
150
|
+
|
|
151
|
+
const minecart = new THREE.Group()
|
|
152
|
+
sceneOrigin.track(minecart)
|
|
153
|
+
minecart.position.set(100, 64, 200)
|
|
154
|
+
|
|
155
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
156
|
+
sceneOrigin.track(localPlayer)
|
|
157
|
+
|
|
158
|
+
anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
159
|
+
minecart.position.set(105, 65, 210)
|
|
160
|
+
sceneOrigin.update(112, 70, 208)
|
|
161
|
+
anchorMinecartPassengers(minecart, sceneOrigin, [LOCAL_PLAYER_ID], localPlayer, {})
|
|
162
|
+
|
|
163
|
+
const detachedWorldPosition = sceneOrigin.getWorldPosition(localPlayer)
|
|
164
|
+
expect(releaseVehiclePassengerPosition(localPlayer, MINECART_ID, detachedWorldPosition)).toBe(true)
|
|
165
|
+
expect(localPlayer.userData._passengerVehicleId).toBeUndefined()
|
|
166
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(detachedWorldPosition)
|
|
167
|
+
|
|
168
|
+
minecart.position.set(120, 66, 220)
|
|
169
|
+
sceneOrigin.update(128, 76, 228)
|
|
170
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(detachedWorldPosition)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
function shouldAnchorPassenger(passenger: PassengerLike | null | undefined, layout: 'boat' | 'minecart' | 'horse', isLocalPassenger: boolean): boolean {
|
|
174
|
+
if (!passenger?.playerObject) return false
|
|
175
|
+
if (isLocalPassenger && layout === 'horse') return false
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test('local player is anchored for boat and minecart but skipped for horse', () => {
|
|
180
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
181
|
+
expect(shouldAnchorPassenger(localPlayer, 'boat', true)).toBe(true)
|
|
182
|
+
expect(shouldAnchorPassenger(localPlayer, 'minecart', true)).toBe(true)
|
|
183
|
+
expect(shouldAnchorPassenger(localPlayer, 'horse', true)).toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('local boat passenger uses the same anchor as remote passenger', () => {
|
|
187
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
188
|
+
sceneOrigin.update(96, 60, 192)
|
|
189
|
+
|
|
190
|
+
const boat = new THREE.Group()
|
|
191
|
+
sceneOrigin.track(boat)
|
|
192
|
+
boat.position.set(100, 64, 200)
|
|
193
|
+
|
|
194
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
195
|
+
const remotePlayer = makePassenger(REMOTE_PLAYER_ID)
|
|
196
|
+
sceneOrigin.track(localPlayer)
|
|
197
|
+
sceneOrigin.track(remotePlayer)
|
|
198
|
+
|
|
199
|
+
const boatSeat = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
|
|
200
|
+
if (shouldAnchorPassenger(localPlayer, 'boat', true)) {
|
|
201
|
+
anchorVehiclePassengerPosition(localPlayer, boatSeat, '10')
|
|
202
|
+
}
|
|
203
|
+
anchorVehiclePassengerPosition(remotePlayer, boatSeat, '10')
|
|
204
|
+
|
|
205
|
+
expect(localPlayer.userData._passengerVehicleId).toBe('10')
|
|
206
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)).toEqual(sceneOrigin.getWorldPosition(remotePlayer))
|
|
207
|
+
expect(sceneOrigin.getWorldPosition(localPlayer)?.y).toBeCloseTo(63.55)
|
|
208
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
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 { WalkingGeneralSwing } from './animations'
|
|
7
|
+
import { anchorVehiclePassengerPosition } from './vehiclePassengerRendering'
|
|
8
|
+
import { applyNetworkHeadPitch, storeNetworkHeadPitch, storeNetworkHeadYaw, type NetworkHeadRotationState } from './networkHeadPitchRendering'
|
|
9
|
+
import { getMinecartPassengerWorldPosition } from './interpolationPolicy'
|
|
10
|
+
|
|
11
|
+
function makePlayerObject(): PlayerObjectType {
|
|
12
|
+
const playerObject = new PlayerObject() as PlayerObjectType
|
|
13
|
+
playerObject.skin.head.rotation.set(0, 0, 0)
|
|
14
|
+
return playerObject
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function simulateAnimationFrames(playerObject: PlayerObjectType, userData: NetworkHeadRotationState, frames: number, dt = 0.05) {
|
|
18
|
+
const animation = new WalkingGeneralSwing()
|
|
19
|
+
animation._captureDefaults(playerObject)
|
|
20
|
+
for (let i = 0; i < frames; i++) {
|
|
21
|
+
animation.update(playerObject, dt)
|
|
22
|
+
applyNetworkHeadPitch(playerObject, userData)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('remote head pitch persists across animation frames', () => {
|
|
27
|
+
const playerObject = makePlayerObject()
|
|
28
|
+
const userData: NetworkHeadRotationState = {}
|
|
29
|
+
storeNetworkHeadPitch(userData, 0.6)
|
|
30
|
+
|
|
31
|
+
simulateAnimationFrames(playerObject, userData, 5)
|
|
32
|
+
|
|
33
|
+
expect(playerObject.skin.head.rotation.x).toBeCloseTo(-0.6)
|
|
34
|
+
expect(playerObject.skin.head.rotation.y).toBe(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('remote head pitch updates when network pitch changes', () => {
|
|
38
|
+
const playerObject = makePlayerObject()
|
|
39
|
+
const userData: NetworkHeadRotationState = {}
|
|
40
|
+
storeNetworkHeadPitch(userData, 0.4)
|
|
41
|
+
simulateAnimationFrames(playerObject, userData, 2)
|
|
42
|
+
expect(playerObject.skin.head.rotation.x).toBeCloseTo(-0.4)
|
|
43
|
+
|
|
44
|
+
storeNetworkHeadPitch(userData, -0.2)
|
|
45
|
+
simulateAnimationFrames(playerObject, userData, 2)
|
|
46
|
+
expect(playerObject.skin.head.rotation.x).toBeCloseTo(0.2)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('remote head pitch supports looking straight ahead', () => {
|
|
50
|
+
const playerObject = makePlayerObject()
|
|
51
|
+
const userData: NetworkHeadRotationState = {}
|
|
52
|
+
storeNetworkHeadPitch(userData, 0)
|
|
53
|
+
simulateAnimationFrames(playerObject, userData, 3)
|
|
54
|
+
expect(playerObject.skin.head.rotation.x).toBeCloseTo(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('local player keeps camera pitch authoritative', () => {
|
|
58
|
+
const playerObject = makePlayerObject()
|
|
59
|
+
const userData: NetworkHeadRotationState = {}
|
|
60
|
+
storeNetworkHeadPitch(userData, 0.8)
|
|
61
|
+
|
|
62
|
+
const cameraPitch = -0.35
|
|
63
|
+
playerObject.skin.head.rotation.x = -cameraPitch
|
|
64
|
+
|
|
65
|
+
expect(playerObject.skin.head.rotation.x).toBeCloseTo(0.35)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('minecart passenger anchor preserves head rotation', () => {
|
|
69
|
+
const playerObject = makePlayerObject()
|
|
70
|
+
const userData: NetworkHeadRotationState = {}
|
|
71
|
+
const passenger = new THREE.Group()
|
|
72
|
+
passenger.userData = userData
|
|
73
|
+
|
|
74
|
+
storeNetworkHeadPitch(userData, 0.45)
|
|
75
|
+
applyNetworkHeadPitch(playerObject, userData)
|
|
76
|
+
|
|
77
|
+
const seat = getMinecartPassengerWorldPosition({ x: 10, y: 64, z: 20 })
|
|
78
|
+
anchorVehiclePassengerPosition(passenger, seat, '42')
|
|
79
|
+
simulateAnimationFrames(playerObject, userData, 4)
|
|
80
|
+
|
|
81
|
+
expect(passenger.position.x).toBe(10)
|
|
82
|
+
expect(passenger.position.y).toBeCloseTo(63.65)
|
|
83
|
+
expect(playerObject.skin.head.rotation.x).toBeCloseTo(-0.45)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('storeNetworkHeadYaw keeps last finite head yaw and ignores invalid updates', () => {
|
|
87
|
+
const userData: NetworkHeadRotationState = {}
|
|
88
|
+
storeNetworkHeadYaw(userData, 1.2)
|
|
89
|
+
expect(userData._networkHeadYaw).toBeCloseTo(1.2)
|
|
90
|
+
|
|
91
|
+
storeNetworkHeadYaw(userData, Number.NaN, undefined)
|
|
92
|
+
expect(userData._networkHeadYaw).toBeCloseTo(1.2)
|
|
93
|
+
|
|
94
|
+
storeNetworkHeadYaw(userData, undefined, 0.8)
|
|
95
|
+
expect(userData._networkHeadYaw).toBeCloseTo(0.8)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('storeNetworkHeadYaw prefers explicit head yaw over fallback', () => {
|
|
99
|
+
const userData: NetworkHeadRotationState = {}
|
|
100
|
+
storeNetworkHeadYaw(userData, 0.5, 1.5)
|
|
101
|
+
expect(userData._networkHeadYaw).toBeCloseTo(0.5)
|
|
102
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type { PlayerObjectType } from '../../lib/createPlayerObject'
|
|
3
|
+
|
|
4
|
+
export type NetworkHeadRotationState = {
|
|
5
|
+
_networkHeadPitch?: number
|
|
6
|
+
_networkHeadYaw?: number
|
|
7
|
+
_remoteBoatRotationApplied?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** @deprecated Use NetworkHeadRotationState */
|
|
11
|
+
export type NetworkHeadPitchState = NetworkHeadRotationState
|
|
12
|
+
|
|
13
|
+
export function storeNetworkHeadPitch(userData: NetworkHeadRotationState, pitch: unknown): void {
|
|
14
|
+
if (typeof pitch === 'number' && Number.isFinite(pitch)) {
|
|
15
|
+
userData._networkHeadPitch = pitch
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function storeNetworkHeadYaw(userData: NetworkHeadRotationState, headYaw: unknown, fallbackYaw?: unknown): void {
|
|
20
|
+
if (typeof headYaw === 'number' && Number.isFinite(headYaw)) {
|
|
21
|
+
userData._networkHeadYaw = headYaw
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (typeof fallbackYaw === 'number' && Number.isFinite(fallbackYaw)) {
|
|
25
|
+
userData._networkHeadYaw = fallbackYaw
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getNetworkHeadPitch(userData: NetworkHeadRotationState): number {
|
|
30
|
+
const pitch = userData._networkHeadPitch
|
|
31
|
+
return typeof pitch === 'number' && Number.isFinite(pitch) ? pitch : 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applyNetworkHeadPitch(playerObject: PlayerObjectType, userData: NetworkHeadRotationState): void {
|
|
35
|
+
const pitch = userData._networkHeadPitch
|
|
36
|
+
if (typeof pitch !== 'number' || !Number.isFinite(pitch)) return
|
|
37
|
+
playerObject.skin.head.rotation.y = 0
|
|
38
|
+
playerObject.skin.head.rotation.x = -pitch
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function restoreGenericRemotePlayerHeadRotation(playerObject: PlayerObjectType, userData: NetworkHeadRotationState): void {
|
|
42
|
+
playerObject.skin.head.rotation.y = 0
|
|
43
|
+
applyNetworkHeadPitch(playerObject, userData)
|
|
44
|
+
}
|
|
@@ -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
|
+
})
|