minecraft-renderer 0.1.93 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/minecraft-renderer.js +64 -64
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +522 -522
- package/package.json +1 -1
- package/src/graphicsBackend/appViewer.ts +4 -3
- package/src/graphicsBackend/types.ts +4 -1
- package/src/lib/worldrendererCommon.ts +2 -2
- package/src/three/entities.ts +303 -33
- package/src/three/entity/EntityMesh.ts +8 -3
- package/src/three/entity/animations.js +37 -25
- package/src/three/entity/animations.test.ts +249 -0
- package/src/three/entity/boatModelRotation.test.ts +29 -0
- package/src/three/entity/boatModelRotation.ts +14 -0
- package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
- package/src/three/entity/boatPaddleAnimation.ts +104 -0
- package/src/three/entity/boatPassengerRendering.ts +20 -0
- package/src/three/entity/boatPassengerRotation.test.ts +166 -0
- package/src/three/entity/boatPassengerRotation.ts +71 -0
- package/src/three/entity/boatRenderSetup.test.ts +236 -0
- package/src/three/entity/boatRenderSetup.ts +214 -0
- package/src/three/entity/entityHeadPose.test.ts +15 -0
- package/src/three/entity/entityHeadPose.ts +22 -0
- package/src/three/entity/horseEntityScale.test.ts +11 -0
- package/src/three/entity/horseEntityScale.ts +9 -0
- package/src/three/entity/horseHeadAnimation.test.ts +159 -0
- package/src/three/entity/horseHeadAnimation.ts +151 -0
- package/src/three/entity/horsePassengerRendering.test.ts +77 -0
- package/src/three/entity/interpolationPolicy.test.ts +281 -0
- package/src/three/entity/interpolationPolicy.ts +242 -0
- package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
- package/src/three/entity/localVehicleRendering.integration.test.ts +267 -0
- package/src/three/entity/minecartLocalPassengerRendering.test.ts +210 -0
- package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
- package/src/three/entity/networkHeadPitchRendering.ts +44 -0
- package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +229 -0
- package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
- package/src/three/entity/vehiclePassengerProduction.test.ts +229 -0
- package/src/three/entity/vehiclePassengerRendering.ts +56 -0
- package/src/three/entity/vehiclePassengerUpdate.ts +91 -0
- package/src/three/graphicsBackendBase.ts +5 -5
- package/src/three/graphicsBackendOffThread.ts +2 -2
- package/src/three/worldRendererThree.ts +48 -22
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
|
|
8
|
+
const RIDING_LEG_X = -1.4137167
|
|
9
|
+
const VANILLA_RIDING_LEG_Y = Math.PI / 10
|
|
10
|
+
const VANILLA_RIDING_LEG_Z = Math.PI / 40
|
|
11
|
+
const SKINVIEW_RIDING_LEG_Y = -VANILLA_RIDING_LEG_Y
|
|
12
|
+
const SKINVIEW_RIDING_LEG_Z = -VANILLA_RIDING_LEG_Z
|
|
13
|
+
const RIDING_LEG_ORDER = 'ZYX'
|
|
14
|
+
const RIDING_ARM_DELTA = Math.PI / 5
|
|
15
|
+
|
|
16
|
+
const YZ_AXIS_FLIP = new THREE.Matrix4().makeScale(1, -1, -1)
|
|
17
|
+
|
|
18
|
+
function makePlayerObject(): PlayerObjectType {
|
|
19
|
+
const playerObject = new PlayerObject() as PlayerObjectType
|
|
20
|
+
playerObject.skin.leftLeg.rotation.set(0.1, 0.05, -0.02)
|
|
21
|
+
playerObject.skin.rightLeg.rotation.set(-0.08, -0.04, 0.03)
|
|
22
|
+
playerObject.skin.leftArm.rotation.set(0.2, 0, 0.01)
|
|
23
|
+
playerObject.skin.rightArm.rotation.set(-0.15, 0, -0.01)
|
|
24
|
+
return playerObject
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function captureRotations(playerObject: PlayerObjectType) {
|
|
28
|
+
const { leftArm, rightArm, leftLeg, rightLeg } = playerObject.skin
|
|
29
|
+
return {
|
|
30
|
+
leftArm: leftArm.rotation.clone(),
|
|
31
|
+
rightArm: rightArm.rotation.clone(),
|
|
32
|
+
leftLeg: leftLeg.rotation.clone(),
|
|
33
|
+
rightLeg: rightLeg.rotation.clone()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runAnimationFrame(animation: WalkingGeneralSwing, playerObject: PlayerObjectType, dt = 0.05) {
|
|
38
|
+
animation.update(playerObject, dt)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeJavaRidingLegMatrix(ySign: 1 | -1): THREE.Matrix4 {
|
|
42
|
+
return new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(RIDING_LEG_X, ySign * VANILLA_RIDING_LEG_Y, ySign * VANILLA_RIDING_LEG_Z, RIDING_LEG_ORDER))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeSkinviewRidingLegMatrix(y: number, z: number): THREE.Matrix4 {
|
|
46
|
+
return new THREE.Matrix4().makeRotationFromEuler(new THREE.Euler(RIDING_LEG_X, y, z, RIDING_LEG_ORDER))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function conjugateYzAxisFlip(rotationMatrix: THREE.Matrix4): THREE.Matrix4 {
|
|
50
|
+
return YZ_AXIS_FLIP.clone().multiply(rotationMatrix).multiply(YZ_AXIS_FLIP.clone())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function expectMatrixNear(a: THREE.Matrix4, b: THREE.Matrix4) {
|
|
54
|
+
for (let i = 0; i < 16; i++) {
|
|
55
|
+
expect(a.elements[i]).toBeCloseTo(b.elements[i])
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expectRidingLegPose(playerObject: PlayerObjectType) {
|
|
60
|
+
expect(playerObject.skin.rightLeg.rotation.order).toBe(RIDING_LEG_ORDER)
|
|
61
|
+
expect(playerObject.skin.rightLeg.rotation.x).toBeCloseTo(RIDING_LEG_X)
|
|
62
|
+
expect(playerObject.skin.rightLeg.rotation.y).toBeCloseTo(SKINVIEW_RIDING_LEG_Y)
|
|
63
|
+
expect(playerObject.skin.rightLeg.rotation.z).toBeCloseTo(SKINVIEW_RIDING_LEG_Z)
|
|
64
|
+
|
|
65
|
+
expect(playerObject.skin.leftLeg.rotation.order).toBe(RIDING_LEG_ORDER)
|
|
66
|
+
expect(playerObject.skin.leftLeg.rotation.x).toBeCloseTo(RIDING_LEG_X)
|
|
67
|
+
expect(playerObject.skin.leftLeg.rotation.y).toBeCloseTo(-SKINVIEW_RIDING_LEG_Y)
|
|
68
|
+
expect(playerObject.skin.leftLeg.rotation.z).toBeCloseTo(-SKINVIEW_RIDING_LEG_Z)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('riding sets ZYX leg rotations with inverted Y/Z signs for skinview3d axes', () => {
|
|
72
|
+
const playerObject = makePlayerObject()
|
|
73
|
+
const animation = new WalkingGeneralSwing()
|
|
74
|
+
animation._captureDefaults(playerObject)
|
|
75
|
+
animation.isRiding = true
|
|
76
|
+
|
|
77
|
+
runAnimationFrame(animation, playerObject)
|
|
78
|
+
|
|
79
|
+
expectRidingLegPose(playerObject)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('riding leg rotation matrix matches vanilla after Y/Z axis conversion', () => {
|
|
83
|
+
const javaRight = makeJavaRidingLegMatrix(1)
|
|
84
|
+
const skinviewRight = makeSkinviewRidingLegMatrix(SKINVIEW_RIDING_LEG_Y, SKINVIEW_RIDING_LEG_Z)
|
|
85
|
+
expectMatrixNear(conjugateYzAxisFlip(skinviewRight), javaRight)
|
|
86
|
+
|
|
87
|
+
const javaLeft = makeJavaRidingLegMatrix(-1)
|
|
88
|
+
const skinviewLeft = makeSkinviewRidingLegMatrix(-SKINVIEW_RIDING_LEG_Y, -SKINVIEW_RIDING_LEG_Z)
|
|
89
|
+
expectMatrixNear(conjugateYzAxisFlip(skinviewLeft), javaLeft)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('animated riding legs match expected rotation matrices', () => {
|
|
93
|
+
const playerObject = makePlayerObject()
|
|
94
|
+
const animation = new WalkingGeneralSwing()
|
|
95
|
+
animation._captureDefaults(playerObject)
|
|
96
|
+
animation.isRiding = true
|
|
97
|
+
runAnimationFrame(animation, playerObject)
|
|
98
|
+
|
|
99
|
+
playerObject.skin.rightLeg.updateMatrix()
|
|
100
|
+
playerObject.skin.leftLeg.updateMatrix()
|
|
101
|
+
|
|
102
|
+
const rightRot = new THREE.Matrix4().extractRotation(playerObject.skin.rightLeg.matrix)
|
|
103
|
+
const leftRot = new THREE.Matrix4().extractRotation(playerObject.skin.leftLeg.matrix)
|
|
104
|
+
|
|
105
|
+
expectMatrixNear(rightRot, makeSkinviewRidingLegMatrix(SKINVIEW_RIDING_LEG_Y, SKINVIEW_RIDING_LEG_Z))
|
|
106
|
+
expectMatrixNear(leftRot, makeSkinviewRidingLegMatrix(-SKINVIEW_RIDING_LEG_Y, -SKINVIEW_RIDING_LEG_Z))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('riding applies arm X delta relative to defaults', () => {
|
|
110
|
+
const playerObject = makePlayerObject()
|
|
111
|
+
const defaults = captureRotations(playerObject)
|
|
112
|
+
const animation = new WalkingGeneralSwing()
|
|
113
|
+
animation._captureDefaults(playerObject)
|
|
114
|
+
animation.isRiding = true
|
|
115
|
+
|
|
116
|
+
runAnimationFrame(animation, playerObject)
|
|
117
|
+
|
|
118
|
+
expect(playerObject.skin.rightArm.rotation.x).toBeCloseTo(defaults.rightArm.x - RIDING_ARM_DELTA)
|
|
119
|
+
expect(playerObject.skin.leftArm.rotation.x).toBeCloseTo(defaults.leftArm.x - RIDING_ARM_DELTA)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('riding does not apply old arm Z spread', () => {
|
|
123
|
+
const playerObject = makePlayerObject()
|
|
124
|
+
const defaults = captureRotations(playerObject)
|
|
125
|
+
const animation = new WalkingGeneralSwing()
|
|
126
|
+
animation._captureDefaults(playerObject)
|
|
127
|
+
animation.isRiding = true
|
|
128
|
+
|
|
129
|
+
runAnimationFrame(animation, playerObject)
|
|
130
|
+
|
|
131
|
+
expect(playerObject.skin.leftArm.rotation.z).toBeCloseTo(defaults.leftArm.z)
|
|
132
|
+
expect(playerObject.skin.rightArm.rotation.z).toBeCloseTo(defaults.rightArm.z)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('idle to riding to idle restores limb rotations including order', () => {
|
|
136
|
+
const playerObject = makePlayerObject()
|
|
137
|
+
const animation = new WalkingGeneralSwing()
|
|
138
|
+
animation._captureDefaults(playerObject)
|
|
139
|
+
const defaults = captureRotations(playerObject)
|
|
140
|
+
|
|
141
|
+
animation.isMoving = false
|
|
142
|
+
runAnimationFrame(animation, playerObject)
|
|
143
|
+
const idleRotations = captureRotations(playerObject)
|
|
144
|
+
|
|
145
|
+
animation.isRiding = true
|
|
146
|
+
runAnimationFrame(animation, playerObject)
|
|
147
|
+
expectRidingLegPose(playerObject)
|
|
148
|
+
|
|
149
|
+
animation.isRiding = false
|
|
150
|
+
animation.isMoving = false
|
|
151
|
+
runAnimationFrame(animation, playerObject)
|
|
152
|
+
const restored = captureRotations(playerObject)
|
|
153
|
+
|
|
154
|
+
expect(restored.leftArm.x).toBeCloseTo(idleRotations.leftArm.x)
|
|
155
|
+
expect(restored.leftArm.y).toBeCloseTo(idleRotations.leftArm.y)
|
|
156
|
+
expect(restored.leftArm.z).toBeCloseTo(idleRotations.leftArm.z)
|
|
157
|
+
expect(restored.leftArm.order).toBe(idleRotations.leftArm.order)
|
|
158
|
+
expect(restored.rightArm.x).toBeCloseTo(idleRotations.rightArm.x)
|
|
159
|
+
expect(restored.rightArm.y).toBeCloseTo(idleRotations.rightArm.y)
|
|
160
|
+
expect(restored.rightArm.z).toBeCloseTo(idleRotations.rightArm.z)
|
|
161
|
+
expect(restored.rightArm.order).toBe(idleRotations.rightArm.order)
|
|
162
|
+
expect(restored.leftLeg.x).toBeCloseTo(defaults.leftLeg.x)
|
|
163
|
+
expect(restored.leftLeg.y).toBeCloseTo(defaults.leftLeg.y)
|
|
164
|
+
expect(restored.leftLeg.z).toBeCloseTo(defaults.leftLeg.z)
|
|
165
|
+
expect(restored.leftLeg.order).toBe(defaults.leftLeg.order)
|
|
166
|
+
expect(restored.rightLeg.x).toBeCloseTo(defaults.rightLeg.x)
|
|
167
|
+
expect(restored.rightLeg.y).toBeCloseTo(defaults.rightLeg.y)
|
|
168
|
+
expect(restored.rightLeg.z).toBeCloseTo(defaults.rightLeg.z)
|
|
169
|
+
expect(restored.rightLeg.order).toBe(defaults.rightLeg.order)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('multiple riding frames do not accumulate angles', () => {
|
|
173
|
+
const playerObject = makePlayerObject()
|
|
174
|
+
const animation = new WalkingGeneralSwing()
|
|
175
|
+
animation._captureDefaults(playerObject)
|
|
176
|
+
animation.isRiding = true
|
|
177
|
+
|
|
178
|
+
runAnimationFrame(animation, playerObject)
|
|
179
|
+
const firstFrame = captureRotations(playerObject)
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < 10; i++) {
|
|
182
|
+
runAnimationFrame(animation, playerObject)
|
|
183
|
+
}
|
|
184
|
+
const lastFrame = captureRotations(playerObject)
|
|
185
|
+
|
|
186
|
+
expect(lastFrame.leftLeg.x).toBeCloseTo(firstFrame.leftLeg.x)
|
|
187
|
+
expect(lastFrame.leftLeg.y).toBeCloseTo(firstFrame.leftLeg.y)
|
|
188
|
+
expect(lastFrame.leftLeg.z).toBeCloseTo(firstFrame.leftLeg.z)
|
|
189
|
+
expect(lastFrame.leftLeg.order).toBe(firstFrame.leftLeg.order)
|
|
190
|
+
expect(lastFrame.rightLeg.x).toBeCloseTo(firstFrame.rightLeg.x)
|
|
191
|
+
expect(lastFrame.rightLeg.y).toBeCloseTo(firstFrame.rightLeg.y)
|
|
192
|
+
expect(lastFrame.rightLeg.z).toBeCloseTo(firstFrame.rightLeg.z)
|
|
193
|
+
expect(lastFrame.rightLeg.order).toBe(firstFrame.rightLeg.order)
|
|
194
|
+
expect(lastFrame.leftArm.x).toBeCloseTo(firstFrame.leftArm.x)
|
|
195
|
+
expect(lastFrame.rightArm.x).toBeCloseTo(firstFrame.rightArm.x)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('oneSwing during riding keeps isRiding enabled', () => {
|
|
199
|
+
const playerObject = makePlayerObject()
|
|
200
|
+
const animation = new WalkingGeneralSwing()
|
|
201
|
+
animation._captureDefaults(playerObject)
|
|
202
|
+
animation.isRiding = true
|
|
203
|
+
|
|
204
|
+
animation.swingArm()
|
|
205
|
+
runAnimationFrame(animation, playerObject)
|
|
206
|
+
|
|
207
|
+
expect(animation.isRiding).toBe(true)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('oneSwing during riding keeps legs in riding pose', () => {
|
|
211
|
+
const playerObject = makePlayerObject()
|
|
212
|
+
const animation = new WalkingGeneralSwing()
|
|
213
|
+
animation._captureDefaults(playerObject)
|
|
214
|
+
animation.isRiding = true
|
|
215
|
+
|
|
216
|
+
animation.swingArm()
|
|
217
|
+
runAnimationFrame(animation, playerObject)
|
|
218
|
+
|
|
219
|
+
expectRidingLegPose(playerObject)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('leg armor attached to legs follows riding Y/Z rotations', () => {
|
|
223
|
+
const playerObject = makePlayerObject()
|
|
224
|
+
const leftLegArmor = new THREE.Group()
|
|
225
|
+
const rightLegArmor = new THREE.Group()
|
|
226
|
+
playerObject.skin.leftLeg.add(leftLegArmor)
|
|
227
|
+
playerObject.skin.rightLeg.add(rightLegArmor)
|
|
228
|
+
|
|
229
|
+
const animation = new WalkingGeneralSwing()
|
|
230
|
+
animation._captureDefaults(playerObject)
|
|
231
|
+
animation.isRiding = true
|
|
232
|
+
runAnimationFrame(animation, playerObject)
|
|
233
|
+
|
|
234
|
+
expectRidingLegPose(playerObject)
|
|
235
|
+
|
|
236
|
+
leftLegArmor.updateMatrixWorld(true)
|
|
237
|
+
rightLegArmor.updateMatrixWorld(true)
|
|
238
|
+
const leftWorldQuat = new THREE.Quaternion()
|
|
239
|
+
const rightWorldQuat = new THREE.Quaternion()
|
|
240
|
+
leftLegArmor.getWorldQuaternion(leftWorldQuat)
|
|
241
|
+
rightLegArmor.getWorldQuaternion(rightWorldQuat)
|
|
242
|
+
const leftWorldEuler = new THREE.Euler().setFromQuaternion(leftWorldQuat, 'XYZ')
|
|
243
|
+
const rightWorldEuler = new THREE.Euler().setFromQuaternion(rightWorldQuat, 'XYZ')
|
|
244
|
+
|
|
245
|
+
expect(leftWorldEuler.y).not.toBeCloseTo(0)
|
|
246
|
+
expect(leftWorldEuler.z).not.toBeCloseTo(0)
|
|
247
|
+
expect(rightWorldEuler.y).not.toBeCloseTo(0)
|
|
248
|
+
expect(rightWorldEuler.z).not.toBeCloseTo(0)
|
|
249
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
import { BOAT_MESH_YAW_OFFSET, getBoatMeshYawOffset, isBoatEntityName } from './boatModelRotation'
|
|
4
|
+
|
|
5
|
+
test('boat mesh yaw offset is -90°', () => {
|
|
6
|
+
expect(BOAT_MESH_YAW_OFFSET).toBe(-Math.PI / 2)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test.each([
|
|
10
|
+
'boat',
|
|
11
|
+
'chest_boat',
|
|
12
|
+
'oak_boat',
|
|
13
|
+
'spruce_boat',
|
|
14
|
+
'birch_boat',
|
|
15
|
+
'jungle_boat',
|
|
16
|
+
'acacia_boat',
|
|
17
|
+
'dark_oak_boat',
|
|
18
|
+
'mangrove_boat',
|
|
19
|
+
'cherry_boat',
|
|
20
|
+
'bamboo_raft'
|
|
21
|
+
])('detects boat entity %s', name => {
|
|
22
|
+
expect(isBoatEntityName(name)).toBe(true)
|
|
23
|
+
expect(getBoatMeshYawOffset(name)).toBe(BOAT_MESH_YAW_OFFSET)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test.each(['minecart', 'horse', 'player', 'item', 'oak_boat_with_chest'])('ignores non-boat entity %s', name => {
|
|
27
|
+
expect(isBoatEntityName(name)).toBe(false)
|
|
28
|
+
expect(getBoatMeshYawOffset(name)).toBeNull()
|
|
29
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
/** OBJ boat models face -X at yaw=0; physics forward is -Z → rotate mesh locally. */
|
|
3
|
+
export const BOAT_MESH_YAW_OFFSET = -Math.PI / 2
|
|
4
|
+
|
|
5
|
+
export function isBoatEntityName(entityName: string | undefined): boolean {
|
|
6
|
+
if (!entityName) return false
|
|
7
|
+
const name = entityName.toLowerCase()
|
|
8
|
+
if (name === 'boat' || name === 'chest_boat') return true
|
|
9
|
+
return name.endsWith('_boat') || name.endsWith('_raft')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getBoatMeshYawOffset(entityName: string | undefined): number | null {
|
|
13
|
+
return isBoatEntityName(entityName) ? BOAT_MESH_YAW_OFFSET : null
|
|
14
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
BOAT_PADDLE_RADIANS_PER_SECOND,
|
|
6
|
+
BOAT_PADDLE_STEP,
|
|
7
|
+
advanceBoatPaddlePhase,
|
|
8
|
+
createBoatPaddlePivotScratch,
|
|
9
|
+
getBoatPaddleRelativeQuaternion,
|
|
10
|
+
getVanillaBoatPaddleAngles,
|
|
11
|
+
syncBoatPaddleAnimationTargets,
|
|
12
|
+
updateBoatPaddleAnimationState,
|
|
13
|
+
createBoatPaddleAnimationState
|
|
14
|
+
} from './boatPaddleAnimation'
|
|
15
|
+
|
|
16
|
+
test('50ms active advancement equals pi/8', () => {
|
|
17
|
+
expect(advanceBoatPaddlePhase(0, true, 0.05)).toBeCloseTo(BOAT_PADDLE_STEP, 5)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('equal elapsed time split across frame sizes gives the same phase', () => {
|
|
21
|
+
const total = 0.1
|
|
22
|
+
const single = advanceBoatPaddlePhase(0, true, total)
|
|
23
|
+
const split = advanceBoatPaddlePhase(advanceBoatPaddlePhase(0, true, total / 2), true, total / 2)
|
|
24
|
+
expect(split).toBeCloseTo(single, 5)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('inactive paddle resets phase to zero', () => {
|
|
28
|
+
expect(advanceBoatPaddlePhase(Math.PI, false, 0.05)).toBe(0)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('active phase remains within [0, 2pi)', () => {
|
|
32
|
+
let phase = 0
|
|
33
|
+
for (let i = 0; i < 200; i++) {
|
|
34
|
+
phase = advanceBoatPaddlePhase(phase, true, 0.016)
|
|
35
|
+
expect(phase).toBeGreaterThanOrEqual(0)
|
|
36
|
+
expect(phase).toBeLessThan(Math.PI * 2)
|
|
37
|
+
expect(Number.isFinite(phase)).toBe(true)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('vanilla angles at phase 0, pi/4, and pi/2 match extracted formulas', () => {
|
|
42
|
+
const phases = [0, Math.PI / 4, Math.PI / 2] as const
|
|
43
|
+
for (const phase of phases) {
|
|
44
|
+
const angles = getVanillaBoatPaddleAngles(phase, 0)
|
|
45
|
+
const tX = (Math.sin(-phase) + 1) / 2
|
|
46
|
+
const tY = (Math.sin(-phase + 1) + 1) / 2
|
|
47
|
+
expect(angles.xRot).toBeCloseTo(-Math.PI / 3 + (-Math.PI / 12 - -Math.PI / 3) * tX, 5)
|
|
48
|
+
expect(angles.yRot).toBeCloseTo(-Math.PI / 4 + (Math.PI / 2) * tY, 5)
|
|
49
|
+
expect(angles.zRot).toBeCloseTo(Math.PI / 16, 5)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('right-side Y is mirrored', () => {
|
|
54
|
+
const left = getVanillaBoatPaddleAngles(Math.PI / 3, 0)
|
|
55
|
+
const right = getVanillaBoatPaddleAngles(Math.PI / 3, 1)
|
|
56
|
+
expect(right.yRot).toBeCloseTo(Math.PI - left.yRot, 5)
|
|
57
|
+
expect(right.xRot).toBeCloseTo(left.xRot, 5)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('relative quaternion at phase zero is identity', () => {
|
|
61
|
+
const scratch = createBoatPaddlePivotScratch()
|
|
62
|
+
const quat = getBoatPaddleRelativeQuaternion(0, 0, scratch)
|
|
63
|
+
expect(quat.x).toBeCloseTo(0, 5)
|
|
64
|
+
expect(quat.y).toBeCloseTo(0, 5)
|
|
65
|
+
expect(quat.z).toBeCloseTo(0, 5)
|
|
66
|
+
expect(quat.w).toBeCloseTo(1, 5)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('returned phases and quaternions are finite', () => {
|
|
70
|
+
const scratch = createBoatPaddlePivotScratch()
|
|
71
|
+
const phase = advanceBoatPaddlePhase(0, true, 0.05)
|
|
72
|
+
const quat = getBoatPaddleRelativeQuaternion(phase, 1, scratch)
|
|
73
|
+
expect(Number.isFinite(phase)).toBe(true)
|
|
74
|
+
expect(Number.isFinite(quat.x)).toBe(true)
|
|
75
|
+
expect(Number.isFinite(quat.y)).toBe(true)
|
|
76
|
+
expect(Number.isFinite(quat.z)).toBe(true)
|
|
77
|
+
expect(Number.isFinite(quat.w)).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('syncBoatPaddleAnimationTargets resets phase when activity changes', () => {
|
|
81
|
+
const state = createBoatPaddleAnimationState()
|
|
82
|
+
state.leftPhase = 1.2
|
|
83
|
+
state.leftActive = true
|
|
84
|
+
|
|
85
|
+
syncBoatPaddleAnimationTargets(state, false, false)
|
|
86
|
+
expect(state.leftActive).toBe(false)
|
|
87
|
+
expect(state.leftPhase).toBe(0)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('updateBoatPaddleAnimationState advances active paddles and rotates pivots', () => {
|
|
91
|
+
const state = createBoatPaddleAnimationState()
|
|
92
|
+
state.leftActive = true
|
|
93
|
+
const leftPivot = new THREE.Object3D()
|
|
94
|
+
const scratch = createBoatPaddlePivotScratch()
|
|
95
|
+
|
|
96
|
+
updateBoatPaddleAnimationState(state, 0.05, leftPivot, undefined, scratch)
|
|
97
|
+
expect(state.leftPhase).toBeCloseTo(BOAT_PADDLE_STEP, 5)
|
|
98
|
+
expect(leftPivot.quaternion.w).not.toBeCloseTo(1, 3)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('negative or non-finite dt is treated as zero advancement', () => {
|
|
102
|
+
expect(advanceBoatPaddlePhase(0.5, true, Number.NaN)).toBe(0.5)
|
|
103
|
+
expect(advanceBoatPaddlePhase(0.5, true, -1)).toBe(0.5)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('boat paddle radians per second matches vanilla tick rate', () => {
|
|
107
|
+
expect(BOAT_PADDLE_RADIANS_PER_SECOND).toBeCloseTo((Math.PI / 8) * 20, 5)
|
|
108
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
export const BOAT_PADDLE_STEP = Math.PI / 8
|
|
5
|
+
export const BOAT_TICKS_PER_SECOND = 20
|
|
6
|
+
export const BOAT_PADDLE_RADIANS_PER_SECOND = BOAT_PADDLE_STEP * BOAT_TICKS_PER_SECOND
|
|
7
|
+
|
|
8
|
+
export type BoatPaddleAnimationState = {
|
|
9
|
+
leftActive: boolean
|
|
10
|
+
rightActive: boolean
|
|
11
|
+
leftPhase: number
|
|
12
|
+
rightPhase: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type BoatPaddlePivotScratch = {
|
|
16
|
+
current: THREE.Quaternion
|
|
17
|
+
rest: THREE.Quaternion
|
|
18
|
+
delta: THREE.Quaternion
|
|
19
|
+
euler: THREE.Euler
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createBoatPaddlePivotScratch(): BoatPaddlePivotScratch {
|
|
23
|
+
return {
|
|
24
|
+
current: new THREE.Quaternion(),
|
|
25
|
+
rest: new THREE.Quaternion(),
|
|
26
|
+
delta: new THREE.Quaternion(),
|
|
27
|
+
euler: new THREE.Euler(0, 0, 0, 'ZYX')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createBoatPaddleAnimationState(): BoatPaddleAnimationState {
|
|
32
|
+
return {
|
|
33
|
+
leftActive: false,
|
|
34
|
+
rightActive: false,
|
|
35
|
+
leftPhase: 0,
|
|
36
|
+
rightPhase: 0
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function lerp(a: number, b: number, t: number): number {
|
|
41
|
+
return a + (b - a) * t
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function advanceBoatPaddlePhase(phase: number, active: boolean, dt: number): number {
|
|
45
|
+
if (!active) return 0
|
|
46
|
+
const safeDt = Number.isFinite(dt) && dt >= 0 ? dt : 0
|
|
47
|
+
const twoPi = Math.PI * 2
|
|
48
|
+
return (phase + safeDt * BOAT_PADDLE_RADIANS_PER_SECOND) % twoPi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getVanillaBoatPaddleAngles(phase: number, side: 0 | 1): { xRot: number; yRot: number; zRot: number } {
|
|
52
|
+
const xRot = lerp(-Math.PI / 3, -Math.PI / 12, (Math.sin(-phase) + 1) / 2)
|
|
53
|
+
let yRot = lerp(-Math.PI / 4, Math.PI / 4, (Math.sin(-phase + 1) + 1) / 2)
|
|
54
|
+
if (side === 1) yRot = Math.PI - yRot
|
|
55
|
+
const zRot = Math.PI / 16
|
|
56
|
+
return { xRot, yRot, zRot }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function vanillaAnglesToQuaternion(angles: { xRot: number; yRot: number; zRot: number }, target: THREE.Quaternion, euler: THREE.Euler): THREE.Quaternion {
|
|
60
|
+
euler.set(-angles.xRot, angles.yRot, -angles.zRot, 'ZYX')
|
|
61
|
+
return target.setFromEuler(euler)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getBoatPaddleRelativeQuaternion(phase: number, side: 0 | 1, scratch: BoatPaddlePivotScratch): THREE.Quaternion {
|
|
65
|
+
if (phase === 0) {
|
|
66
|
+
return scratch.delta.identity()
|
|
67
|
+
}
|
|
68
|
+
vanillaAnglesToQuaternion(getVanillaBoatPaddleAngles(phase, side), scratch.current, scratch.euler)
|
|
69
|
+
vanillaAnglesToQuaternion(getVanillaBoatPaddleAngles(0, side), scratch.rest, scratch.euler)
|
|
70
|
+
return scratch.delta.copy(scratch.current).multiply(scratch.rest.invert())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function applyBoatPaddlePivotRotation(pivot: THREE.Object3D, phase: number, side: 0 | 1, scratch: BoatPaddlePivotScratch): void {
|
|
74
|
+
pivot.quaternion.copy(getBoatPaddleRelativeQuaternion(phase, side, scratch))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function syncBoatPaddleAnimationTargets(state: BoatPaddleAnimationState, leftActive: boolean, rightActive: boolean): void {
|
|
78
|
+
if (state.leftActive !== leftActive) {
|
|
79
|
+
state.leftActive = leftActive
|
|
80
|
+
state.leftPhase = 0
|
|
81
|
+
}
|
|
82
|
+
if (state.rightActive !== rightActive) {
|
|
83
|
+
state.rightActive = rightActive
|
|
84
|
+
state.rightPhase = 0
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function updateBoatPaddleAnimationState(
|
|
89
|
+
state: BoatPaddleAnimationState,
|
|
90
|
+
dt: number,
|
|
91
|
+
leftPivot: THREE.Object3D | undefined,
|
|
92
|
+
rightPivot: THREE.Object3D | undefined,
|
|
93
|
+
scratch: BoatPaddlePivotScratch
|
|
94
|
+
): void {
|
|
95
|
+
state.leftPhase = advanceBoatPaddlePhase(state.leftPhase, state.leftActive, dt)
|
|
96
|
+
state.rightPhase = advanceBoatPaddlePhase(state.rightPhase, state.rightActive, dt)
|
|
97
|
+
|
|
98
|
+
if (leftPivot) {
|
|
99
|
+
applyBoatPaddlePivotRotation(leftPivot, state.leftPhase, 0, scratch)
|
|
100
|
+
}
|
|
101
|
+
if (rightPivot) {
|
|
102
|
+
applyBoatPaddlePivotRotation(rightPivot, state.rightPhase, 1, scratch)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import type { Vec3Like } from './interpolationPolicy'
|
|
3
|
+
|
|
4
|
+
export type VehiclePassengerRenderState = {
|
|
5
|
+
position: {
|
|
6
|
+
set: (x: number, y: number, z: number) => unknown
|
|
7
|
+
}
|
|
8
|
+
userData: {
|
|
9
|
+
_posTween?: { stop: () => unknown }
|
|
10
|
+
_tweenTarget?: Vec3Like
|
|
11
|
+
_passengerVehicleId?: string
|
|
12
|
+
/** @deprecated Use _passengerVehicleId */
|
|
13
|
+
_boatPassengerVehicleId?: string
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
anchorVehiclePassengerPosition as anchorBoatPassengerPosition,
|
|
19
|
+
releaseVehiclePassengerPosition as releaseBoatPassengerPosition
|
|
20
|
+
} from './vehiclePassengerRendering'
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
BOAT_PASSENGER_MAX_HEAD_YAW_RAD,
|
|
5
|
+
clampBoatPassengerRelativeHeadYaw,
|
|
6
|
+
getClampedBoatPassengerYaw,
|
|
7
|
+
normalizeYaw,
|
|
8
|
+
normalizeYawDelta,
|
|
9
|
+
resolveBoatPassengerThirdPersonRotation,
|
|
10
|
+
shouldApplyBoatPassengerRotation,
|
|
11
|
+
shouldApplyBoatPassengerThirdPersonRotation
|
|
12
|
+
} from './boatPassengerRotation'
|
|
13
|
+
|
|
14
|
+
const BOAT_YAW = -0.5645049299419149
|
|
15
|
+
|
|
16
|
+
test('clamp relative head yaw at ±105° boundaries', () => {
|
|
17
|
+
expect(clampBoatPassengerRelativeHeadYaw(BOAT_PASSENGER_MAX_HEAD_YAW_RAD)).toBeCloseTo(BOAT_PASSENGER_MAX_HEAD_YAW_RAD)
|
|
18
|
+
expect(clampBoatPassengerRelativeHeadYaw(-BOAT_PASSENGER_MAX_HEAD_YAW_RAD)).toBeCloseTo(-BOAT_PASSENGER_MAX_HEAD_YAW_RAD)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('clamp relative head yaw beyond both boundaries', () => {
|
|
22
|
+
const over = BOAT_PASSENGER_MAX_HEAD_YAW_RAD + 0.5
|
|
23
|
+
const under = -BOAT_PASSENGER_MAX_HEAD_YAW_RAD - 0.5
|
|
24
|
+
expect(clampBoatPassengerRelativeHeadYaw(over)).toBeCloseTo(BOAT_PASSENGER_MAX_HEAD_YAW_RAD)
|
|
25
|
+
expect(clampBoatPassengerRelativeHeadYaw(under)).toBeCloseTo(-BOAT_PASSENGER_MAX_HEAD_YAW_RAD)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('normalizeYawDelta handles π/-π seam', () => {
|
|
29
|
+
expect(normalizeYawDelta(Math.PI, -Math.PI)).toBeCloseTo(0)
|
|
30
|
+
expect(normalizeYawDelta(-Math.PI, Math.PI)).toBeCloseTo(0)
|
|
31
|
+
expect(normalizeYawDelta(0.1, 2 * Math.PI + 0.1)).toBeCloseTo(0)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('normalizeYaw wraps 2π to 0', () => {
|
|
35
|
+
expect(normalizeYaw(2 * Math.PI)).toBeCloseTo(0)
|
|
36
|
+
expect(normalizeYaw(-2 * Math.PI)).toBeCloseTo(0)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('getClampedBoatPassengerYaw clamps absolute look beyond 105° from boat', () => {
|
|
40
|
+
const requested = BOAT_YAW + 2.5
|
|
41
|
+
const clamped = getClampedBoatPassengerYaw(requested, BOAT_YAW)
|
|
42
|
+
expect(normalizeYawDelta(BOAT_YAW, clamped)).toBeCloseTo(BOAT_PASSENGER_MAX_HEAD_YAW_RAD)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('renderer and camera share the same effective yaw', () => {
|
|
46
|
+
const cameraYaw = BOAT_YAW + 1.9
|
|
47
|
+
const clampedLook = getClampedBoatPassengerYaw(cameraYaw, BOAT_YAW)
|
|
48
|
+
const resolved = resolveBoatPassengerThirdPersonRotation({
|
|
49
|
+
cameraYaw,
|
|
50
|
+
cameraPitch: 0.2,
|
|
51
|
+
vehicleYaw: BOAT_YAW
|
|
52
|
+
})
|
|
53
|
+
expect(resolved.effectiveCameraYaw).toBeCloseTo(clampedLook)
|
|
54
|
+
expect(normalizeYaw(BOAT_YAW + resolved.headYaw)).toBeCloseTo(clampedLook)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('positive camera delta yields positive local head yaw', () => {
|
|
58
|
+
const cameraYaw = BOAT_YAW + 0.5
|
|
59
|
+
const resolved = resolveBoatPassengerThirdPersonRotation({
|
|
60
|
+
cameraYaw,
|
|
61
|
+
cameraPitch: 0,
|
|
62
|
+
vehicleYaw: BOAT_YAW
|
|
63
|
+
})
|
|
64
|
+
expect(resolved.headYaw).toBeGreaterThan(0)
|
|
65
|
+
expect(resolved.headYaw).toBeCloseTo(normalizeYawDelta(BOAT_YAW, cameraYaw))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('head yaw sign follows normalize(cameraYaw - vehicleYaw) without inversion', () => {
|
|
69
|
+
const positive = resolveBoatPassengerThirdPersonRotation({
|
|
70
|
+
cameraYaw: BOAT_YAW + 0.3,
|
|
71
|
+
cameraPitch: 0,
|
|
72
|
+
vehicleYaw: BOAT_YAW
|
|
73
|
+
})
|
|
74
|
+
const negative = resolveBoatPassengerThirdPersonRotation({
|
|
75
|
+
cameraYaw: BOAT_YAW - 0.3,
|
|
76
|
+
cameraPitch: 0,
|
|
77
|
+
vehicleYaw: BOAT_YAW
|
|
78
|
+
})
|
|
79
|
+
expect(positive.headYaw).toBeCloseTo(0.3)
|
|
80
|
+
expect(negative.headYaw).toBeCloseTo(-0.3)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('shouldApplyBoatPassengerRotation excludes minecart and horse without third-person requirement', () => {
|
|
84
|
+
const base = {
|
|
85
|
+
isAnchoredPassenger: true,
|
|
86
|
+
vehicleYaw: BOAT_YAW
|
|
87
|
+
}
|
|
88
|
+
expect(shouldApplyBoatPassengerRotation({ ...base, vehicleName: 'boat' })).toBe(true)
|
|
89
|
+
expect(shouldApplyBoatPassengerRotation({ ...base, vehicleName: 'minecart' })).toBe(false)
|
|
90
|
+
expect(shouldApplyBoatPassengerRotation({ ...base, vehicleName: 'horse' })).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('shouldApplyBoatPassengerThirdPersonRotation excludes minecart and horse', () => {
|
|
94
|
+
const base = {
|
|
95
|
+
isThirdPerson: true,
|
|
96
|
+
isAnchoredPassenger: true,
|
|
97
|
+
vehicleYaw: BOAT_YAW
|
|
98
|
+
}
|
|
99
|
+
expect(shouldApplyBoatPassengerThirdPersonRotation({ ...base, vehicleName: 'boat' })).toBe(true)
|
|
100
|
+
expect(shouldApplyBoatPassengerThirdPersonRotation({ ...base, vehicleName: 'minecart' })).toBe(false)
|
|
101
|
+
expect(shouldApplyBoatPassengerThirdPersonRotation({ ...base, vehicleName: 'horse' })).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('first mount frame without anchor uses camera fallback gate', () => {
|
|
105
|
+
expect(
|
|
106
|
+
shouldApplyBoatPassengerThirdPersonRotation({
|
|
107
|
+
isThirdPerson: true,
|
|
108
|
+
isAnchoredPassenger: false,
|
|
109
|
+
vehicleName: 'boat',
|
|
110
|
+
vehicleYaw: BOAT_YAW
|
|
111
|
+
})
|
|
112
|
+
).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('first dismount frame without anchor uses camera fallback gate', () => {
|
|
116
|
+
expect(
|
|
117
|
+
shouldApplyBoatPassengerThirdPersonRotation({
|
|
118
|
+
isThirdPerson: true,
|
|
119
|
+
isAnchoredPassenger: false,
|
|
120
|
+
vehicleName: undefined,
|
|
121
|
+
vehicleYaw: undefined
|
|
122
|
+
})
|
|
123
|
+
).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('non-finite or missing vehicle yaw disables boat branch', () => {
|
|
127
|
+
expect(
|
|
128
|
+
shouldApplyBoatPassengerThirdPersonRotation({
|
|
129
|
+
isThirdPerson: true,
|
|
130
|
+
isAnchoredPassenger: true,
|
|
131
|
+
vehicleName: 'boat',
|
|
132
|
+
vehicleYaw: Number.NaN
|
|
133
|
+
})
|
|
134
|
+
).toBe(false)
|
|
135
|
+
expect(getClampedBoatPassengerYaw(1.2, Number.NaN)).toBe(1.2)
|
|
136
|
+
expect(getClampedBoatPassengerYaw(Number.NaN, BOAT_YAW)).toBeNaN()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('acceptance identity: headYaw ≈ clamp(normalize(cameraYaw - boatYaw))', () => {
|
|
140
|
+
const cameraYaw = BOAT_YAW + 1.1
|
|
141
|
+
const resolved = resolveBoatPassengerThirdPersonRotation({
|
|
142
|
+
cameraYaw,
|
|
143
|
+
cameraPitch: -0.4,
|
|
144
|
+
vehicleYaw: BOAT_YAW
|
|
145
|
+
})
|
|
146
|
+
const expectedHead = clampBoatPassengerRelativeHeadYaw(normalizeYawDelta(BOAT_YAW, cameraYaw))
|
|
147
|
+
expect(resolved.bodyYaw).toBeCloseTo(BOAT_YAW)
|
|
148
|
+
expect(resolved.headYaw).toBeCloseTo(expectedHead)
|
|
149
|
+
expect(resolved.effectiveCameraYaw).toBeCloseTo(normalizeYaw(BOAT_YAW + expectedHead))
|
|
150
|
+
expect(resolved.headPitch).toBeCloseTo(0.4)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('helmet head armor copies skin head yaw after boat passenger rotation', () => {
|
|
154
|
+
const resolved = resolveBoatPassengerThirdPersonRotation({
|
|
155
|
+
cameraYaw: BOAT_YAW + 0.45,
|
|
156
|
+
cameraPitch: -0.2,
|
|
157
|
+
vehicleYaw: BOAT_YAW
|
|
158
|
+
})
|
|
159
|
+
const skinHead = { x: resolved.headPitch, y: resolved.headYaw, z: 0 }
|
|
160
|
+
const helmet = { x: 0, y: 0, z: 0 }
|
|
161
|
+
helmet.x = -skinHead.x
|
|
162
|
+
helmet.y = skinHead.y
|
|
163
|
+
helmet.z = skinHead.z
|
|
164
|
+
expect(helmet.y).toBeCloseTo(resolved.headYaw)
|
|
165
|
+
expect(helmet.x).toBeCloseTo(-resolved.headPitch)
|
|
166
|
+
})
|