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,247 @@
|
|
|
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
|
+
function staleOffsetVehiclePosition(cameraWorldPos: Vec3, vehiclePosition: Vec3, stalePassengerPosition: Vec3): Vec3 {
|
|
18
|
+
return {
|
|
19
|
+
x: cameraWorldPos.x + (vehiclePosition.x - stalePassengerPosition.x),
|
|
20
|
+
y: cameraWorldPos.y + (vehiclePosition.y - stalePassengerPosition.y),
|
|
21
|
+
z: cameraWorldPos.z + (vehiclePosition.z - stalePassengerPosition.z)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('stale passenger snapshot does not shift rendered locally ridden vehicle X/Z relative to camera', () => {
|
|
26
|
+
const stalePassenger = { x: 0, y: 63, z: 0 }
|
|
27
|
+
const newVehicle = { x: 1, y: 63.2, z: 2 }
|
|
28
|
+
const cameraWorldPos = { x: 0.4, y: 63.5, z: 0.8 }
|
|
29
|
+
|
|
30
|
+
// 1. Vehicle position updates before passenger sync.
|
|
31
|
+
// 2. Renderer receives vehicle snapshot while passenger is still stale.
|
|
32
|
+
const broken = staleOffsetVehiclePosition(cameraWorldPos, newVehicle, stalePassenger)
|
|
33
|
+
const rendered = getLocalVehicleWorldPosition(cameraWorldPos, newVehicle.y)
|
|
34
|
+
|
|
35
|
+
expect(rendered.x).toBe(cameraWorldPos.x)
|
|
36
|
+
expect(rendered.z).toBe(cameraWorldPos.z)
|
|
37
|
+
expect(rendered.y).toBe(newVehicle.y)
|
|
38
|
+
expect(broken.x).not.toBe(cameraWorldPos.x)
|
|
39
|
+
expect(broken.z).not.toBe(cameraWorldPos.z)
|
|
40
|
+
|
|
41
|
+
// 3. Passenger synchronizes to vehicle.
|
|
42
|
+
const syncedPassenger = { ...newVehicle, y: 63 }
|
|
43
|
+
const syncedCamera = { x: 0.8, y: 63.5, z: 1.6 }
|
|
44
|
+
|
|
45
|
+
// 4. Boat X/Z remain fixed relative to the player/camera on intermediate frames.
|
|
46
|
+
const afterSync = getLocalVehicleWorldPosition(syncedCamera, newVehicle.y)
|
|
47
|
+
expect(afterSync.x - syncedCamera.x).toBe(0)
|
|
48
|
+
expect(afterSync.z - syncedCamera.z).toBe(0)
|
|
49
|
+
expect(afterSync.y).toBe(newVehicle.y)
|
|
50
|
+
expect(syncedPassenger.x).toBe(newVehicle.x)
|
|
51
|
+
expect(syncedPassenger.z).toBe(newVehicle.z)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('render frames between press and release keep zero horizontal delta to camera for local boat', () => {
|
|
55
|
+
const vehicleY = 62.75
|
|
56
|
+
const frames = [
|
|
57
|
+
{ x: 0, y: 63.4, z: 0 },
|
|
58
|
+
{ x: 0.2, y: 63.4, z: 0 },
|
|
59
|
+
{ x: 0.45, y: 63.4, z: 0 },
|
|
60
|
+
{ x: 0.7, y: 63.4, z: 0 },
|
|
61
|
+
{ x: 0.7, y: 63.4, z: 0 },
|
|
62
|
+
{ x: 0.55, y: 63.4, z: 0 }
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
for (const camera of frames) {
|
|
66
|
+
const boat = getLocalVehicleWorldPosition(camera, vehicleY)
|
|
67
|
+
expect(boat.x - camera.x).toBe(0)
|
|
68
|
+
expect(boat.z - camera.z).toBe(0)
|
|
69
|
+
expect(boat.y).toBe(vehicleY)
|
|
70
|
+
expect(boat.y).not.toBe(camera.y)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('boat passenger positions use vanilla 1.17.1 riding and seat offsets', () => {
|
|
75
|
+
const boat = { x: 10, y: 64, z: 20 }
|
|
76
|
+
|
|
77
|
+
expect(getBoatPassengerWorldPosition(boat, 0, 0, 1)).toEqual({
|
|
78
|
+
x: 10,
|
|
79
|
+
y: 63.55,
|
|
80
|
+
z: 20
|
|
81
|
+
})
|
|
82
|
+
expect(getBoatPassengerWorldPosition(boat, 0, 0, 2)).toEqual({
|
|
83
|
+
x: 10,
|
|
84
|
+
y: 63.55,
|
|
85
|
+
z: 20.2
|
|
86
|
+
})
|
|
87
|
+
expect(getBoatPassengerWorldPosition(boat, 0, 1, 2)).toEqual({
|
|
88
|
+
x: 10,
|
|
89
|
+
y: 63.55,
|
|
90
|
+
z: 19.4
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const rotated = getBoatPassengerWorldPosition(boat, Math.PI / 2, 0, 2)
|
|
94
|
+
expect(rotated.x).toBeCloseTo(9.8)
|
|
95
|
+
expect(rotated.y).toBe(63.55)
|
|
96
|
+
expect(rotated.z).toBeCloseTo(20)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('remote player follows the tracked boat each frame and releases from an empty passenger list', () => {
|
|
100
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
101
|
+
sceneOrigin.update(96, 60, 192)
|
|
102
|
+
|
|
103
|
+
const boat = new THREE.Group()
|
|
104
|
+
sceneOrigin.track(boat)
|
|
105
|
+
boat.position.set(100, 64, 200)
|
|
106
|
+
|
|
107
|
+
const passenger = new THREE.Group()
|
|
108
|
+
const stopPositionTween = vi.fn()
|
|
109
|
+
passenger.userData._posTween = { stop: stopPositionTween }
|
|
110
|
+
sceneOrigin.track(passenger)
|
|
111
|
+
passenger.position.set(90, 64, 190)
|
|
112
|
+
|
|
113
|
+
const firstPassengerPosition = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
|
|
114
|
+
anchorVehiclePassengerPosition(passenger, firstPassengerPosition, '10')
|
|
115
|
+
|
|
116
|
+
expect(stopPositionTween).toHaveBeenCalledOnce()
|
|
117
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual({ x: 100, y: 63.55, z: 200 })
|
|
118
|
+
expect(passenger.userData._tweenTarget).toEqual({ x: 100, y: 63.55, z: 200 })
|
|
119
|
+
|
|
120
|
+
boat.position.set(102, 64.2, 203)
|
|
121
|
+
boat.rotation.y = Math.PI / 2
|
|
122
|
+
const movedPassengerPosition = getBoatPassengerWorldPosition(sceneOrigin.getWorldPosition(boat)!, boat.rotation.y, 0, 1)
|
|
123
|
+
anchorVehiclePassengerPosition(passenger, movedPassengerPosition, '10')
|
|
124
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.x).toBe(102)
|
|
125
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.y).toBeCloseTo(63.75)
|
|
126
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.z).toBe(203)
|
|
127
|
+
|
|
128
|
+
sceneOrigin.update(112, 70, 208)
|
|
129
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.x).toBe(102)
|
|
130
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.y).toBeCloseTo(63.75)
|
|
131
|
+
expect(sceneOrigin.getWorldPosition(passenger)?.z).toBe(203)
|
|
132
|
+
|
|
133
|
+
const detachedWorldPosition = sceneOrigin.getWorldPosition(passenger)
|
|
134
|
+
expect(releaseVehiclePassengerPosition(passenger, '10', detachedWorldPosition)).toBe(true)
|
|
135
|
+
expect(passenger.userData._passengerVehicleId).toBeUndefined()
|
|
136
|
+
expect(passenger.userData._boatPassengerVehicleId).toBeUndefined()
|
|
137
|
+
expect(passenger.userData._tweenTarget).toEqual(detachedWorldPosition)
|
|
138
|
+
|
|
139
|
+
boat.position.set(110, 65, 210)
|
|
140
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual(detachedWorldPosition)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('minecart passenger uses vanilla 1.17.1 riding Y offset', () => {
|
|
144
|
+
const minecart = { x: 10, y: 64, z: 20 }
|
|
145
|
+
expect(getMinecartPassengerWorldPosition(minecart)).toEqual({
|
|
146
|
+
x: 10,
|
|
147
|
+
y: 63.65,
|
|
148
|
+
z: 20
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('minecart passenger anchor follows vehicle world position without inheriting vehicle yaw', () => {
|
|
153
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
154
|
+
sceneOrigin.update(96, 60, 192)
|
|
155
|
+
|
|
156
|
+
const minecart = new THREE.Group()
|
|
157
|
+
sceneOrigin.track(minecart)
|
|
158
|
+
minecart.position.set(100, 64, 200)
|
|
159
|
+
minecart.rotation.y = Math.PI / 2
|
|
160
|
+
|
|
161
|
+
const passenger = new THREE.Group()
|
|
162
|
+
passenger.rotation.y = 1.25
|
|
163
|
+
sceneOrigin.track(passenger)
|
|
164
|
+
passenger.position.set(90, 64, 190)
|
|
165
|
+
|
|
166
|
+
const firstPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
|
|
167
|
+
anchorVehiclePassengerPosition(passenger, firstPosition, '42')
|
|
168
|
+
const anchoredYaw = passenger.rotation.y
|
|
169
|
+
|
|
170
|
+
minecart.position.set(103, 64.5, 205)
|
|
171
|
+
minecart.rotation.y = Math.PI
|
|
172
|
+
const movedPosition = getMinecartPassengerWorldPosition(sceneOrigin.getWorldPosition(minecart)!)
|
|
173
|
+
anchorVehiclePassengerPosition(passenger, movedPosition, '42')
|
|
174
|
+
|
|
175
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual({
|
|
176
|
+
x: 103,
|
|
177
|
+
y: 64.15,
|
|
178
|
+
z: 205
|
|
179
|
+
})
|
|
180
|
+
expect(passenger.rotation.y).toBe(anchoredYaw)
|
|
181
|
+
|
|
182
|
+
const detachedWorldPosition = sceneOrigin.getWorldPosition(passenger)
|
|
183
|
+
expect(releaseVehiclePassengerPosition(passenger, '42', detachedWorldPosition)).toBe(true)
|
|
184
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual(detachedWorldPosition)
|
|
185
|
+
|
|
186
|
+
minecart.position.set(120, 66, 220)
|
|
187
|
+
expect(sceneOrigin.getWorldPosition(passenger)).toEqual(detachedWorldPosition)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('local minecart with localVehicle uses camera-synced positioning', () => {
|
|
191
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { localVehicle: true, passengerLayout: 'minecart' } })).toBe(true)
|
|
192
|
+
expect(getEntityTweenDurationMs({ renderHints: { localVehicle: true, passengerLayout: 'minecart' } }, false)).toBe(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('remote minecart keeps ordinary entity interpolation', () => {
|
|
196
|
+
expect(usesCameraSyncedVehiclePosition({ renderHints: { passengerLayout: 'minecart' } })).toBe(false)
|
|
197
|
+
expect(getEntityTweenDurationMs({ renderHints: { passengerLayout: 'minecart' } }, false)).toBe(ENTITY_TWEEN_DURATION_MS)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('local minecart X/Z stay aligned with camera on intermediate frames between server targets', () => {
|
|
201
|
+
const vehicleY = 63.7
|
|
202
|
+
const cameraFrames = [
|
|
203
|
+
{ x: 0, y: 64.3, z: 0 },
|
|
204
|
+
{ x: 1.2, y: 64.3, z: 0.6 },
|
|
205
|
+
{ x: 5, y: 64.3, z: 3 },
|
|
206
|
+
{ x: 4.1, y: 64.3, z: 2.5 }
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
for (const camera of cameraFrames) {
|
|
210
|
+
const minecart = getLocalVehicleWorldPosition(camera, vehicleY)
|
|
211
|
+
expect(minecart.x - camera.x).toBeCloseTo(0, 5)
|
|
212
|
+
expect(minecart.z - camera.z).toBeCloseTo(0, 5)
|
|
213
|
+
expect(minecart.y).toBe(vehicleY)
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('local minecart passenger anchors to camera-synced vehicle position', () => {
|
|
218
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
219
|
+
sceneOrigin.update(96, 60, 192)
|
|
220
|
+
|
|
221
|
+
const minecart = new THREE.Group()
|
|
222
|
+
sceneOrigin.track(minecart)
|
|
223
|
+
|
|
224
|
+
const passenger = new THREE.Group()
|
|
225
|
+
sceneOrigin.track(passenger)
|
|
226
|
+
|
|
227
|
+
const vehicleY = 64
|
|
228
|
+
const cameraFrames = [
|
|
229
|
+
{ x: 100, y: 64.7, z: 200 },
|
|
230
|
+
{ x: 101.4, y: 64.7, z: 200.8 },
|
|
231
|
+
{ x: 103, y: 64.7, z: 202 }
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
for (const camera of cameraFrames) {
|
|
235
|
+
const syncedMinecart = getLocalVehicleWorldPosition(camera, vehicleY)
|
|
236
|
+
minecart.position.set(syncedMinecart.x, syncedMinecart.y, syncedMinecart.z)
|
|
237
|
+
const seatPosition = getMinecartPassengerWorldPosition(syncedMinecart)
|
|
238
|
+
anchorVehiclePassengerPosition(passenger, seatPosition, '42')
|
|
239
|
+
const passengerWorld = sceneOrigin.getWorldPosition(passenger)!
|
|
240
|
+
|
|
241
|
+
expect(syncedMinecart.x - camera.x).toBeCloseTo(0, 5)
|
|
242
|
+
expect(syncedMinecart.z - camera.z).toBeCloseTo(0, 5)
|
|
243
|
+
expect(passengerWorld.x).toBe(syncedMinecart.x)
|
|
244
|
+
expect(passengerWorld.z).toBe(syncedMinecart.z)
|
|
245
|
+
expect(passengerWorld.y).toBeCloseTo(63.65)
|
|
246
|
+
}
|
|
247
|
+
})
|
|
@@ -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
|
+
}
|