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.
Files changed (43) hide show
  1. package/dist/minecraft-renderer.js +68 -68
  2. package/dist/minecraft-renderer.js.meta.json +1 -1
  3. package/dist/threeWorker.js +522 -522
  4. package/package.json +1 -1
  5. package/src/graphicsBackend/appViewer.ts +4 -3
  6. package/src/graphicsBackend/config.ts +1 -1
  7. package/src/graphicsBackend/types.ts +4 -1
  8. package/src/lib/worldrendererCommon.ts +3 -3
  9. package/src/three/entities.ts +329 -43
  10. package/src/three/entity/EntityMesh.ts +8 -3
  11. package/src/three/entity/animations.js +37 -25
  12. package/src/three/entity/animations.test.ts +249 -0
  13. package/src/three/entity/boatModelRotation.test.ts +29 -0
  14. package/src/three/entity/boatModelRotation.ts +14 -0
  15. package/src/three/entity/boatPaddleAnimation.test.ts +108 -0
  16. package/src/three/entity/boatPaddleAnimation.ts +104 -0
  17. package/src/three/entity/boatPassengerRendering.ts +20 -0
  18. package/src/three/entity/boatPassengerRotation.test.ts +166 -0
  19. package/src/three/entity/boatPassengerRotation.ts +71 -0
  20. package/src/three/entity/boatRenderSetup.test.ts +236 -0
  21. package/src/three/entity/boatRenderSetup.ts +214 -0
  22. package/src/three/entity/entityHeadPose.test.ts +15 -0
  23. package/src/three/entity/entityHeadPose.ts +22 -0
  24. package/src/three/entity/horseEntityScale.test.ts +11 -0
  25. package/src/three/entity/horseEntityScale.ts +9 -0
  26. package/src/three/entity/horseHeadAnimation.test.ts +159 -0
  27. package/src/three/entity/horseHeadAnimation.ts +151 -0
  28. package/src/three/entity/horsePassengerRendering.test.ts +77 -0
  29. package/src/three/entity/interpolationPolicy.test.ts +259 -0
  30. package/src/three/entity/interpolationPolicy.ts +187 -0
  31. package/src/three/entity/localHorseCameraYawLock.test.ts +72 -0
  32. package/src/three/entity/localVehicleRendering.integration.test.ts +247 -0
  33. package/src/three/entity/minecartLocalPassengerRendering.test.ts +208 -0
  34. package/src/three/entity/networkHeadPitchRendering.test.ts +102 -0
  35. package/src/three/entity/networkHeadPitchRendering.ts +44 -0
  36. package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +227 -0
  37. package/src/three/entity/remoteBoatPassengerRotation.ts +128 -0
  38. package/src/three/entity/vehiclePassengerProduction.test.ts +214 -0
  39. package/src/three/entity/vehiclePassengerRendering.ts +56 -0
  40. package/src/three/entity/vehiclePassengerUpdate.ts +82 -0
  41. package/src/three/graphicsBackendBase.ts +5 -5
  42. package/src/three/graphicsBackendOffThread.ts +2 -2
  43. 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,9 @@
1
+ //@ts-nocheck
2
+ /** Vanilla 1.17.1 AbstractHorseRenderer entity scales (model OBJ root only). */
3
+ export const VANILLA_117_HORSE_ENTITY_SCALES: Record<string, number> = {
4
+ horse: 1.1,
5
+ donkey: 0.87,
6
+ mule: 0.92,
7
+ skeleton_horse: 1.0,
8
+ zombie_horse: 1.0
9
+ }
@@ -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
+ })