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,15 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
import { resolveEntityHeadPose } from './entityHeadPose'
|
|
4
|
+
|
|
5
|
+
test('uses entity pitch and network head yaw as separate angles', () => {
|
|
6
|
+
expect(resolveEntityHeadPose({ pitch: 0.25, yaw: 1, headYaw: -0.5 })).toEqual({ pitch: 0.25, headYaw: -0.5 })
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('falls back to body yaw when head yaw is unavailable', () => {
|
|
10
|
+
expect(resolveEntityHeadPose({ pitch: -0.2, yaw: 1.25 })).toEqual({ pitch: -0.2, headYaw: 1.25 })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('does not propagate missing or non-finite rotations', () => {
|
|
14
|
+
expect(resolveEntityHeadPose({ pitch: Number.NaN, yaw: Number.POSITIVE_INFINITY, headYaw: undefined })).toEqual({ pitch: 0, headYaw: 0 })
|
|
15
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
export type EntityHeadPoseInput = {
|
|
3
|
+
pitch?: unknown
|
|
4
|
+
yaw?: unknown
|
|
5
|
+
headYaw?: unknown
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type EntityHeadPose = {
|
|
9
|
+
pitch: number
|
|
10
|
+
headYaw: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveEntityHeadPose(entity: EntityHeadPoseInput): EntityHeadPose {
|
|
14
|
+
const pitch = typeof entity.pitch === 'number' && Number.isFinite(entity.pitch) ? entity.pitch : 0
|
|
15
|
+
const headYaw =
|
|
16
|
+
typeof entity.headYaw === 'number' && Number.isFinite(entity.headYaw)
|
|
17
|
+
? entity.headYaw
|
|
18
|
+
: typeof entity.yaw === 'number' && Number.isFinite(entity.yaw)
|
|
19
|
+
? entity.yaw
|
|
20
|
+
: 0
|
|
21
|
+
return { pitch, headYaw }
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { expect, test } from 'vitest'
|
|
3
|
+
import { VANILLA_117_HORSE_ENTITY_SCALES } from './horseEntityScale'
|
|
4
|
+
|
|
5
|
+
test('vanilla 1.17.1 horse family entity scales', () => {
|
|
6
|
+
expect(VANILLA_117_HORSE_ENTITY_SCALES.horse).toBe(1.1)
|
|
7
|
+
expect(VANILLA_117_HORSE_ENTITY_SCALES.donkey).toBe(0.87)
|
|
8
|
+
expect(VANILLA_117_HORSE_ENTITY_SCALES.mule).toBe(0.92)
|
|
9
|
+
expect(VANILLA_117_HORSE_ENTITY_SCALES.skeleton_horse).toBe(1.0)
|
|
10
|
+
expect(VANILLA_117_HORSE_ENTITY_SCALES.zombie_horse).toBe(1.0)
|
|
11
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
HORSE_HEAD_MAX_YAW,
|
|
6
|
+
applyHorseHeadPose,
|
|
7
|
+
calculateHorseHeadPose,
|
|
8
|
+
createHorseHeadAnimationState,
|
|
9
|
+
createHorseHeadRig,
|
|
10
|
+
getInterpolatedHorseHeadAnimation,
|
|
11
|
+
HORSE_HEAD_TICK_SECONDS,
|
|
12
|
+
setHorseHeadAnimationPosition,
|
|
13
|
+
updateHorseHeadAnimationFrame
|
|
14
|
+
} from './horseHeadAnimation'
|
|
15
|
+
|
|
16
|
+
test('head rig keeps world bounds and includes duplicate head parts', () => {
|
|
17
|
+
const root = new THREE.Group()
|
|
18
|
+
const body = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1))
|
|
19
|
+
body.name = 'Body'
|
|
20
|
+
const head = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1))
|
|
21
|
+
head.name = 'Head'
|
|
22
|
+
head.position.set(0, 2, -1)
|
|
23
|
+
const headSaddleA = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1))
|
|
24
|
+
headSaddleA.name = 'HeadSaddle'
|
|
25
|
+
headSaddleA.position.set(0, 1.5, -1)
|
|
26
|
+
const headSaddleB = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1))
|
|
27
|
+
headSaddleB.name = 'HeadSaddle'
|
|
28
|
+
headSaddleB.position.set(0, 1.8, -1)
|
|
29
|
+
root.add(body, head, headSaddleA, headSaddleB)
|
|
30
|
+
root.updateMatrixWorld(true)
|
|
31
|
+
const before = new THREE.Box3().setFromObject(root)
|
|
32
|
+
|
|
33
|
+
const rig = createHorseHeadRig(root)
|
|
34
|
+
root.updateMatrixWorld(true)
|
|
35
|
+
const after = new THREE.Box3().setFromObject(root)
|
|
36
|
+
|
|
37
|
+
expect(rig.userData.horseHeadParts).toHaveLength(3)
|
|
38
|
+
expect(rig.userData.horseHeadParts).toContain(head)
|
|
39
|
+
expect(rig.userData.horseHeadParts.filter(part => part.name === 'HeadSaddle')).toHaveLength(2)
|
|
40
|
+
expect(after.min.toArray()).toEqual(before.min.toArray())
|
|
41
|
+
expect(after.max.toArray()).toEqual(before.max.toArray())
|
|
42
|
+
expect(root.children.filter(child => child.name === 'horse_head_rig')).toHaveLength(1)
|
|
43
|
+
expect(createHorseHeadRig(root)).toBe(rig)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('horse head pitch follows entity pitch in radians', () => {
|
|
47
|
+
const pose = calculateHorseHeadPose({
|
|
48
|
+
entityPitch: -Math.PI / 6,
|
|
49
|
+
headYaw: 0,
|
|
50
|
+
bodyYaw: 0,
|
|
51
|
+
limbSwing: 0,
|
|
52
|
+
limbSwingAmount: 0
|
|
53
|
+
})
|
|
54
|
+
expect(pose.pitch).toBeCloseTo(-Math.PI / 6)
|
|
55
|
+
expect(pose.yaw).toBe(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('horse gait subtracts from pitch at full limb amount', () => {
|
|
59
|
+
const pose = calculateHorseHeadPose({
|
|
60
|
+
entityPitch: 0,
|
|
61
|
+
headYaw: 0,
|
|
62
|
+
bodyYaw: 0,
|
|
63
|
+
limbSwing: 0,
|
|
64
|
+
limbSwingAmount: 1
|
|
65
|
+
})
|
|
66
|
+
expect(pose.pitch).toBeCloseTo(-0.15)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('horse gait is disabled for a small limb amount', () => {
|
|
70
|
+
expect(
|
|
71
|
+
calculateHorseHeadPose({
|
|
72
|
+
entityPitch: 0.3,
|
|
73
|
+
headYaw: 0,
|
|
74
|
+
bodyYaw: 0,
|
|
75
|
+
limbSwing: Math.PI,
|
|
76
|
+
limbSwingAmount: 0.2
|
|
77
|
+
}).pitch
|
|
78
|
+
).toBeCloseTo(0.3)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('relative head yaw is shortest-path and limited to twenty degrees', () => {
|
|
82
|
+
expect(
|
|
83
|
+
calculateHorseHeadPose({
|
|
84
|
+
entityPitch: 0,
|
|
85
|
+
headYaw: -Math.PI + 0.1,
|
|
86
|
+
bodyYaw: Math.PI - 0.1,
|
|
87
|
+
limbSwing: 0,
|
|
88
|
+
limbSwingAmount: 0
|
|
89
|
+
}).yaw
|
|
90
|
+
).toBeCloseTo(0.2)
|
|
91
|
+
expect(
|
|
92
|
+
calculateHorseHeadPose({
|
|
93
|
+
entityPitch: 0,
|
|
94
|
+
headYaw: Math.PI / 2,
|
|
95
|
+
bodyYaw: 0,
|
|
96
|
+
limbSwing: 0,
|
|
97
|
+
limbSwingAmount: 0
|
|
98
|
+
}).yaw
|
|
99
|
+
).toBeCloseTo(HORSE_HEAD_MAX_YAW)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('animation phase uses authoritative horizontal distance and interpolates between ticks', () => {
|
|
103
|
+
const state = createHorseHeadAnimationState()
|
|
104
|
+
setHorseHeadAnimationPosition(state, { x: 10, z: 10 })
|
|
105
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
106
|
+
setHorseHeadAnimationPosition(state, { x: 10.1, z: 10 })
|
|
107
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
108
|
+
expect(state.limbSwingAmount).toBeCloseTo(0.16)
|
|
109
|
+
expect(getInterpolatedHorseHeadAnimation(state, 0).limbSwingAmount).toBe(0)
|
|
110
|
+
expect(getInterpolatedHorseHeadAnimation(state, 1).limbSwingAmount).toBeCloseTo(0.16)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('movement position is consumed once per render tick', () => {
|
|
114
|
+
const state = createHorseHeadAnimationState()
|
|
115
|
+
setHorseHeadAnimationPosition(state, { x: 10, z: 10 })
|
|
116
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
117
|
+
setHorseHeadAnimationPosition(state, { x: 10.1, z: 10 })
|
|
118
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
119
|
+
expect(state.limbSwingAmount).toBeCloseTo(0.16)
|
|
120
|
+
|
|
121
|
+
setHorseHeadAnimationPosition(state, { x: 10.2, z: 10 })
|
|
122
|
+
expect(state.limbSwingAmount).toBeCloseTo(0.16)
|
|
123
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
124
|
+
expect(state.limbSwingAmount).toBeCloseTo(0.256)
|
|
125
|
+
expect(state.limbSwing).toBeCloseTo(0.416)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('render frames decay gait when a remote horse stops sending movement updates', () => {
|
|
129
|
+
const state = createHorseHeadAnimationState()
|
|
130
|
+
setHorseHeadAnimationPosition(state, { x: 10, z: 10 })
|
|
131
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
132
|
+
setHorseHeadAnimationPosition(state, { x: 10.1, z: 10 })
|
|
133
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
134
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
135
|
+
expect(state.limbSwingAmount).toBeCloseTo(0.096)
|
|
136
|
+
expect(state.limbSwing).toBeCloseTo(0.256)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('interpolates the leftover time after a tick at 30 FPS', () => {
|
|
140
|
+
const state = createHorseHeadAnimationState()
|
|
141
|
+
setHorseHeadAnimationPosition(state, { x: 10, z: 10 })
|
|
142
|
+
updateHorseHeadAnimationFrame(state, HORSE_HEAD_TICK_SECONDS)
|
|
143
|
+
setHorseHeadAnimationPosition(state, { x: 10.1, z: 10 })
|
|
144
|
+
|
|
145
|
+
updateHorseHeadAnimationFrame(state, 0.033)
|
|
146
|
+
const interpolated = updateHorseHeadAnimationFrame(state, 0.033)
|
|
147
|
+
|
|
148
|
+
expect(state.tickAccumulatorSeconds).toBeCloseTo(0.016)
|
|
149
|
+
expect(state.elapsedSeconds / HORSE_HEAD_TICK_SECONDS).toBeCloseTo(0.32)
|
|
150
|
+
expect(interpolated.limbSwingAmount).toBeCloseTo(0.16 * 0.32)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('applying a pose changes only the runtime rig', () => {
|
|
154
|
+
const rig = new THREE.Group()
|
|
155
|
+
applyHorseHeadPose(rig, { pitch: 0.4, yaw: -0.2 })
|
|
156
|
+
expect(rig.rotation.x).toBeCloseTo(0.4)
|
|
157
|
+
expect(rig.rotation.y).toBeCloseTo(-0.2)
|
|
158
|
+
expect(rig.rotation.z).toBe(0)
|
|
159
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
export const HORSE_HEAD_PIVOT = { x: 0, y: 1.25, z: -0.75 } as const
|
|
5
|
+
export const HORSE_HEAD_MAX_YAW = THREE.MathUtils.degToRad(20)
|
|
6
|
+
export const HORSE_HEAD_TICK_SECONDS = 0.05
|
|
7
|
+
|
|
8
|
+
const HORSE_HEAD_PART_NAMES = new Set([
|
|
9
|
+
'Head',
|
|
10
|
+
'UMouth',
|
|
11
|
+
'Ear1',
|
|
12
|
+
'Ear2',
|
|
13
|
+
'MuleEarL',
|
|
14
|
+
'MuleEarR',
|
|
15
|
+
'Neck',
|
|
16
|
+
'Mane',
|
|
17
|
+
'SaddleMouthL',
|
|
18
|
+
'SaddleMouthR',
|
|
19
|
+
'SaddleMouthLine',
|
|
20
|
+
'SaddleMouthLineR',
|
|
21
|
+
'HeadSaddle'
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
export type HorseHeadRig = THREE.Group & {
|
|
25
|
+
userData: THREE.Group['userData'] & { horseHeadParts: THREE.Object3D[] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const HORSE_HEAD_RIG_USER_DATA_KEY = '_horseHeadRig'
|
|
29
|
+
export const HORSE_HEAD_ANIMATION_USER_DATA_KEY = '_horseHeadAnimation'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reparents every horse head-related OBJ object under the vanilla head_parts
|
|
33
|
+
* pivot while retaining each object's world transform.
|
|
34
|
+
*/
|
|
35
|
+
export function createHorseHeadRig(root: THREE.Object3D): HorseHeadRig {
|
|
36
|
+
const existing = root.userData[HORSE_HEAD_RIG_USER_DATA_KEY] as HorseHeadRig | undefined
|
|
37
|
+
if (existing) return existing
|
|
38
|
+
|
|
39
|
+
const parts: THREE.Object3D[] = []
|
|
40
|
+
root.traverse(child => {
|
|
41
|
+
if (child !== root && HORSE_HEAD_PART_NAMES.has(child.name)) parts.push(child)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const rig = new THREE.Group() as HorseHeadRig
|
|
45
|
+
rig.name = 'horse_head_rig'
|
|
46
|
+
rig.position.set(HORSE_HEAD_PIVOT.x, HORSE_HEAD_PIVOT.y, HORSE_HEAD_PIVOT.z)
|
|
47
|
+
rig.userData.horseHeadParts = parts
|
|
48
|
+
root.add(rig)
|
|
49
|
+
|
|
50
|
+
for (const part of parts) rig.attach(part)
|
|
51
|
+
|
|
52
|
+
root.userData[HORSE_HEAD_RIG_USER_DATA_KEY] = rig
|
|
53
|
+
return rig
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type HorseHeadPoseInput = {
|
|
57
|
+
entityPitch: number
|
|
58
|
+
headYaw: number
|
|
59
|
+
bodyYaw: number
|
|
60
|
+
limbSwing: number
|
|
61
|
+
limbSwingAmount: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type HorseHeadPose = {
|
|
65
|
+
pitch: number
|
|
66
|
+
yaw: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeYawDelta(fromYaw: number, toYaw: number): number {
|
|
70
|
+
const tau = Math.PI * 2
|
|
71
|
+
const delta = ((((toYaw - fromYaw + Math.PI) % tau) + tau) % tau) - Math.PI
|
|
72
|
+
return delta === -Math.PI ? Math.PI : delta
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function calculateHorseHeadPose(input: HorseHeadPoseInput): HorseHeadPose {
|
|
76
|
+
const { entityPitch, headYaw, bodyYaw, limbSwing, limbSwingAmount } = input
|
|
77
|
+
const safeAmount = Number.isFinite(limbSwingAmount) ? Math.max(0, limbSwingAmount) : 0
|
|
78
|
+
const gait = safeAmount > 0.2 ? Math.cos(limbSwing * 0.4) * 0.15 * safeAmount : 0
|
|
79
|
+
const relativeYaw = THREE.MathUtils.clamp(normalizeYawDelta(bodyYaw, headYaw), -HORSE_HEAD_MAX_YAW, HORSE_HEAD_MAX_YAW)
|
|
80
|
+
return {
|
|
81
|
+
pitch: (Number.isFinite(entityPitch) ? entityPitch : 0) - gait,
|
|
82
|
+
yaw: relativeYaw
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type HorseHeadAnimationState = {
|
|
87
|
+
lastPosition?: { x: number; z: number }
|
|
88
|
+
pendingPosition?: { x: number; z: number }
|
|
89
|
+
prevLimbSwing: number
|
|
90
|
+
limbSwing: number
|
|
91
|
+
prevLimbSwingAmount: number
|
|
92
|
+
limbSwingAmount: number
|
|
93
|
+
elapsedSeconds: number
|
|
94
|
+
tickAccumulatorSeconds: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createHorseHeadAnimationState(): HorseHeadAnimationState {
|
|
98
|
+
return {
|
|
99
|
+
prevLimbSwing: 0,
|
|
100
|
+
limbSwing: 0,
|
|
101
|
+
prevLimbSwingAmount: 0,
|
|
102
|
+
limbSwingAmount: 0,
|
|
103
|
+
elapsedSeconds: HORSE_HEAD_TICK_SECONDS,
|
|
104
|
+
tickAccumulatorSeconds: 0
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Save authoritative entity coordinates until the next animation tick. */
|
|
109
|
+
export function setHorseHeadAnimationPosition(state: HorseHeadAnimationState, position: { x: number; z: number }): void {
|
|
110
|
+
state.pendingPosition = { x: position.x, z: position.z }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Advance using authoritative entity coordinates, before camera transforms. */
|
|
114
|
+
export function advanceHorseHeadAnimation(state: HorseHeadAnimationState, position: { x: number; z: number }): void {
|
|
115
|
+
state.prevLimbSwing = state.limbSwing
|
|
116
|
+
state.prevLimbSwingAmount = state.limbSwingAmount
|
|
117
|
+
|
|
118
|
+
const distance = state.lastPosition == null ? 0 : Math.hypot(position.x - state.lastPosition.x, position.z - state.lastPosition.z)
|
|
119
|
+
const targetAmount = Math.min(1, distance * 4)
|
|
120
|
+
state.limbSwingAmount += (targetAmount - state.limbSwingAmount) * 0.4
|
|
121
|
+
state.limbSwing += state.limbSwingAmount
|
|
122
|
+
state.lastPosition = { x: position.x, z: position.z }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getInterpolatedHorseHeadAnimation(state: HorseHeadAnimationState, partialTick: number): { limbSwing: number; limbSwingAmount: number } {
|
|
126
|
+
const alpha = THREE.MathUtils.clamp(partialTick, 0, 1)
|
|
127
|
+
return {
|
|
128
|
+
limbSwing: state.prevLimbSwing + (state.limbSwing - state.prevLimbSwing) * alpha,
|
|
129
|
+
limbSwingAmount: state.prevLimbSwingAmount + (state.limbSwingAmount - state.prevLimbSwingAmount) * alpha
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function updateHorseHeadAnimationFrame(state: HorseHeadAnimationState, deltaSeconds: number): { limbSwing: number; limbSwingAmount: number } {
|
|
134
|
+
const safeDeltaSeconds = Math.max(0, deltaSeconds)
|
|
135
|
+
state.elapsedSeconds = Math.min(HORSE_HEAD_TICK_SECONDS, state.elapsedSeconds + safeDeltaSeconds)
|
|
136
|
+
state.tickAccumulatorSeconds += safeDeltaSeconds
|
|
137
|
+
|
|
138
|
+
while (state.tickAccumulatorSeconds >= HORSE_HEAD_TICK_SECONDS) {
|
|
139
|
+
const position = state.pendingPosition ?? state.lastPosition
|
|
140
|
+
if (position) advanceHorseHeadAnimation(state, position)
|
|
141
|
+
state.pendingPosition = undefined
|
|
142
|
+
state.tickAccumulatorSeconds -= HORSE_HEAD_TICK_SECONDS
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
state.elapsedSeconds = state.tickAccumulatorSeconds
|
|
146
|
+
return getInterpolatedHorseHeadAnimation(state, state.elapsedSeconds / HORSE_HEAD_TICK_SECONDS)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function applyHorseHeadPose(rig: THREE.Object3D, pose: HorseHeadPose): void {
|
|
150
|
+
rig.rotation.set(pose.pitch, pose.yaw, 0)
|
|
151
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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 { getHorsePassengerWorldPosition } from './interpolationPolicy'
|
|
7
|
+
|
|
8
|
+
const LOCAL_PLAYER_ID = 7
|
|
9
|
+
const REMOTE_PLAYER_ID = 8
|
|
10
|
+
const HORSE_ID = '42'
|
|
11
|
+
const HORSE_HEIGHT = 1.6
|
|
12
|
+
|
|
13
|
+
type PassengerLike = THREE.Group & {
|
|
14
|
+
playerObject: Record<string, unknown>
|
|
15
|
+
originalEntity: { id: number }
|
|
16
|
+
userData: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makePassenger(id: number): PassengerLike {
|
|
20
|
+
const passenger = new THREE.Group() as PassengerLike
|
|
21
|
+
passenger.playerObject = {}
|
|
22
|
+
passenger.originalEntity = { id }
|
|
23
|
+
passenger.userData = {}
|
|
24
|
+
return passenger
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test.each([
|
|
28
|
+
['horse', 1.6, 0.85],
|
|
29
|
+
['zombie_horse', 1.6, 0.85],
|
|
30
|
+
['donkey', 1.5, 0.525],
|
|
31
|
+
['mule', 1.6, 0.6],
|
|
32
|
+
['skeleton_horse', 1.6, 0.6625]
|
|
33
|
+
])('horse variant %s seat Y uses vanilla offset', (name, height, expectedFeetOffset) => {
|
|
34
|
+
const seat = getHorsePassengerWorldPosition({ x: 0, y: 64, z: 0 }, name, height)
|
|
35
|
+
expect(seat.y).toBeCloseTo(64 + expectedFeetOffset, 5)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('local and remote horse passengers share the same anchor', () => {
|
|
39
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
40
|
+
sceneOrigin.update(96, 60, 192)
|
|
41
|
+
|
|
42
|
+
const horse = new THREE.Group()
|
|
43
|
+
sceneOrigin.track(horse)
|
|
44
|
+
horse.position.set(100, 64, 200)
|
|
45
|
+
|
|
46
|
+
const localPlayer = makePassenger(LOCAL_PLAYER_ID)
|
|
47
|
+
const remotePlayer = makePassenger(REMOTE_PLAYER_ID)
|
|
48
|
+
sceneOrigin.track(localPlayer)
|
|
49
|
+
sceneOrigin.track(remotePlayer)
|
|
50
|
+
|
|
51
|
+
const vehicleWorldPos = sceneOrigin.getWorldPosition(horse)!
|
|
52
|
+
const seatPosition = getHorsePassengerWorldPosition(vehicleWorldPos, 'horse', HORSE_HEIGHT)
|
|
53
|
+
|
|
54
|
+
anchorVehiclePassengerPosition(localPlayer, seatPosition, HORSE_ID)
|
|
55
|
+
anchorVehiclePassengerPosition(remotePlayer, seatPosition, HORSE_ID)
|
|
56
|
+
|
|
57
|
+
const localWorld = sceneOrigin.getWorldPosition(localPlayer)
|
|
58
|
+
const remoteWorld = sceneOrigin.getWorldPosition(remotePlayer)
|
|
59
|
+
expect(localWorld).toEqual(remoteWorld)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('passenger anchor releases after detach', () => {
|
|
63
|
+
const sceneOrigin = new SceneOrigin(new THREE.Scene())
|
|
64
|
+
sceneOrigin.update(0, 0, 0)
|
|
65
|
+
|
|
66
|
+
const horse = new THREE.Group()
|
|
67
|
+
sceneOrigin.track(horse)
|
|
68
|
+
horse.position.set(0, 64, 0)
|
|
69
|
+
|
|
70
|
+
const passenger = makePassenger(LOCAL_PLAYER_ID)
|
|
71
|
+
sceneOrigin.track(passenger)
|
|
72
|
+
|
|
73
|
+
const seatPosition = getHorsePassengerWorldPosition({ x: 0, y: 64, z: 0 }, 'horse', HORSE_HEIGHT)
|
|
74
|
+
anchorVehiclePassengerPosition(passenger, seatPosition, HORSE_ID)
|
|
75
|
+
releaseVehiclePassengerPosition(passenger, HORSE_ID, sceneOrigin.getWorldPosition(passenger))
|
|
76
|
+
expect(passenger.userData._passengerVehicleId).toBeUndefined()
|
|
77
|
+
})
|