minecraft-renderer 0.1.31 → 0.1.33

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.
@@ -0,0 +1,957 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import * as tweenJs from '@tweenjs/tween.js'
4
+ import PrismarineItem from 'prismarine-item'
5
+ import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
6
+ import { BlockModel } from 'mc-assets'
7
+ import { DebugGui } from '../lib/DebugGui'
8
+ import { SmoothSwitcher } from '../lib/smoothSwitcher'
9
+ import { watchProperty } from '../lib/utils/proxy'
10
+ import { subscribeKey } from 'valtio/utils'
11
+ import { getMyHand } from './handLegacy'
12
+ import { WorldRendererThree } from './worldRendererThree'
13
+ import { disposeObject } from './threeJsUtils'
14
+ import { HandItemBlock, MovementState } from '../playerState/types'
15
+ import { PlayerStateRenderer } from '../playerState/playerState'
16
+ import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
17
+ import { IndexedData } from 'minecraft-data'
18
+ import { WorldRendererConfig } from '../graphicsBackend'
19
+ import { IHoldingBlock } from './holdingBlockTypes'
20
+
21
+ const rotationPositionData = {
22
+ itemRight: {
23
+ 'rotation': [
24
+ 0,
25
+ -90,
26
+ 25
27
+ ],
28
+ 'translation': [
29
+ 1.13,
30
+ 3.2,
31
+ 1.13
32
+ ],
33
+ 'scale': [
34
+ 0.68,
35
+ 0.68,
36
+ 0.68
37
+ ]
38
+ },
39
+ itemLeft: {
40
+ 'rotation': [
41
+ 0,
42
+ 90,
43
+ -25
44
+ ],
45
+ 'translation': [
46
+ 1.13,
47
+ 3.2,
48
+ 1.13
49
+ ],
50
+ 'scale': [
51
+ 0.68,
52
+ 0.68,
53
+ 0.68
54
+ ]
55
+ },
56
+ blockRight: {
57
+ 'rotation': [
58
+ 0,
59
+ 45,
60
+ 0
61
+ ],
62
+ 'translation': [
63
+ 0,
64
+ 0,
65
+ 0
66
+ ],
67
+ 'scale': [
68
+ 0.4,
69
+ 0.4,
70
+ 0.4
71
+ ]
72
+ },
73
+ blockLeft: {
74
+ 'rotation': [
75
+ 0,
76
+ 225,
77
+ 0
78
+ ],
79
+ 'translation': [
80
+ 0,
81
+ 0,
82
+ 0
83
+ ],
84
+ 'scale': [
85
+ 0.4,
86
+ 0.4,
87
+ 0.4
88
+ ]
89
+ }
90
+ }
91
+
92
+ export default class HoldingBlockLegacy implements IHoldingBlock {
93
+ // TODO refactor with the tree builder for better visual understanding
94
+ holdingBlock: THREE.Object3D | undefined = undefined
95
+ blockSwapAnimation: {
96
+ switcher: SmoothSwitcher
97
+ // hidden: boolean
98
+ } | undefined = undefined
99
+ cameraGroup = new THREE.Mesh()
100
+ objectOuterGroup = new THREE.Group() // 3
101
+ objectInnerGroup = new THREE.Group() // 4
102
+ holdingBlockInnerGroup = new THREE.Group() // 5
103
+ camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100)
104
+ stopUpdate = false
105
+ lastHeldItem: HandItemBlock | undefined
106
+ isSwinging = false
107
+ nextIterStopCallbacks: Array<() => void> | undefined
108
+ idleAnimator: HandIdleAnimator | undefined
109
+ ready = false
110
+ lastUpdate = 0
111
+ playerHand: THREE.Object3D | undefined
112
+ offHandDisplay = false
113
+ offHandModeLegacy = false
114
+
115
+ swingAnimator: HandSwingAnimator | undefined
116
+ config: WorldRendererConfig
117
+ private disposed = false
118
+ unsubs: Array<() => void> = []
119
+
120
+ constructor(public worldRenderer: WorldRendererThree, public offHand = false) {
121
+ this.initCameraGroup()
122
+ this.unsubs.push(subscribeKey(this.worldRenderer.playerStateReactive, 'heldItemMain', () => {
123
+ if (!this.offHand) {
124
+ this.updateItem()
125
+ }
126
+ }))
127
+ this.unsubs.push(subscribeKey(this.worldRenderer.playerStateReactive, 'heldItemOff', () => {
128
+ if (this.offHand) {
129
+ this.updateItem()
130
+ }
131
+ }))
132
+ this.config = worldRenderer.displayOptions.inWorldRenderingConfig
133
+
134
+ this.offHandDisplay = this.offHand
135
+ // this.offHandDisplay = true
136
+ if (!this.offHand) {
137
+ // load default hand
138
+ void getMyHand().then((hand) => {
139
+ if (this.disposed) return
140
+ this.playerHand = hand
141
+ // trigger update
142
+ this.updateItem()
143
+ }).then(() => {
144
+ if (this.disposed) return
145
+ // now watch over the player skin
146
+ const unsub = watchProperty(
147
+ async () => {
148
+ return getMyHand(this.worldRenderer.playerStateReactive.playerSkin, this.worldRenderer.playerStateReactive.onlineMode ? this.worldRenderer.playerStateReactive.username : undefined)
149
+ },
150
+ this.worldRenderer.playerStateReactive,
151
+ 'playerSkin',
152
+ (newHand) => {
153
+ if (newHand) {
154
+ this.playerHand = newHand
155
+ // trigger update
156
+ this.updateItem()
157
+ }
158
+ },
159
+ (oldHand) => {
160
+ disposeObject(oldHand!, true)
161
+ }
162
+ )
163
+ this.unsubs.push(unsub)
164
+ })
165
+ }
166
+ }
167
+
168
+ updateItem() {
169
+ if (!this.ready) return
170
+ const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain
171
+ if (item) {
172
+ void this.setNewItem(item)
173
+ } else if (this.offHand) {
174
+ void this.setNewItem()
175
+ } else {
176
+ void this.setNewItem({
177
+ type: 'hand',
178
+ })
179
+ }
180
+ }
181
+
182
+ initCameraGroup() {
183
+ this.cameraGroup = new THREE.Mesh()
184
+ }
185
+
186
+ startSwing() {
187
+ this.swingAnimator?.startSwing()
188
+ }
189
+
190
+ stopSwing() {
191
+ this.swingAnimator?.stopSwing()
192
+ }
193
+
194
+ render(originalCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer, ambientLight: THREE.AmbientLight, directionalLight: THREE.DirectionalLight) {
195
+ if (!this.lastHeldItem) return
196
+ const now = performance.now()
197
+ if (this.lastUpdate && now - this.lastUpdate > 50) { // one tick
198
+ void this.replaceItemModel(this.lastHeldItem)
199
+ }
200
+
201
+ // Only update idle animation if not swinging
202
+ if (this.swingAnimator?.isCurrentlySwinging() || this.swingAnimator?.debugParams.animationStage) {
203
+ this.swingAnimator?.update()
204
+ } else {
205
+ this.idleAnimator?.update()
206
+ }
207
+
208
+ this.blockSwapAnimation?.switcher.update()
209
+
210
+ const scene = new THREE.Scene()
211
+ scene.add(this.cameraGroup)
212
+ // if (this.camera.aspect !== originalCamera.aspect) {
213
+ // this.camera.aspect = originalCamera.aspect
214
+ // this.camera.updateProjectionMatrix()
215
+ // }
216
+ this.updateCameraGroup()
217
+ scene.add(ambientLight.clone())
218
+ scene.add(directionalLight.clone())
219
+
220
+ const viewerSize = renderer.getSize(new THREE.Vector2())
221
+ const minSize = Math.min(viewerSize.width, viewerSize.height)
222
+ const x = viewerSize.width - minSize
223
+
224
+ // Mirror the scene for offhand by scaling
225
+ const { offHandDisplay } = this
226
+ if (offHandDisplay) {
227
+ this.cameraGroup.scale.x = -1
228
+ }
229
+
230
+ renderer.autoClear = false
231
+ renderer.clearDepth()
232
+ if (this.offHandDisplay) {
233
+ renderer.setViewport(0, 0, minSize, minSize)
234
+ } else {
235
+ const x = viewerSize.width - minSize
236
+ // if (x) x -= x / 4
237
+ renderer.setViewport(x, 0, minSize, minSize)
238
+ }
239
+ renderer.render(scene, this.camera)
240
+ renderer.setViewport(0, 0, viewerSize.width, viewerSize.height)
241
+
242
+ // Reset the mirroring after rendering
243
+ if (offHandDisplay) {
244
+ this.cameraGroup.scale.x = 1
245
+ }
246
+ }
247
+
248
+ // worldTest () {
249
+ // const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
250
+ // mesh.position.set(0.5, 0.5, 0.5)
251
+ // const group = new THREE.Group()
252
+ // group.add(mesh)
253
+ // group.position.set(-0.5, -0.5, -0.5)
254
+ // const outerGroup = new THREE.Group()
255
+ // outerGroup.add(group)
256
+ // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
257
+ // this.scene.add(outerGroup)
258
+
259
+ // new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
260
+ // }
261
+
262
+ async playBlockSwapAnimation(forceState: 'appeared' | 'disappeared') {
263
+ this.blockSwapAnimation ??= {
264
+ switcher: new SmoothSwitcher(
265
+ () => ({
266
+ y: this.objectInnerGroup.position.y
267
+ }),
268
+ (property, value) => {
269
+ if (property === 'y') this.objectInnerGroup.position.y = value
270
+ },
271
+ {
272
+ y: 16 // units per second
273
+ }
274
+ )
275
+ }
276
+
277
+ const newState = forceState
278
+ // if (forceState && newState !== forceState) {
279
+ // throw new Error(`forceState does not match current state ${forceState} !== ${newState}`)
280
+ // }
281
+
282
+ const targetY = this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (newState === 'appeared' ? 1 : -1))
283
+
284
+ // if (newState === this.blockSwapAnimation.switcher.transitioningToStateName) {
285
+ // return false
286
+ // }
287
+
288
+ let cancelled = false
289
+ return new Promise<boolean>((resolve) => {
290
+ this.blockSwapAnimation!.switcher.transitionTo(
291
+ { y: targetY },
292
+ newState,
293
+ () => {
294
+ if (!cancelled) {
295
+ resolve(true)
296
+ }
297
+ },
298
+ () => {
299
+ cancelled = true
300
+ resolve(false)
301
+ }
302
+ )
303
+ })
304
+ }
305
+
306
+ isDifferentItem(block: HandItemBlock | undefined) {
307
+ const Item = PrismarineItem(this.worldRenderer.version)
308
+ if (!this.lastHeldItem) {
309
+ return true
310
+ }
311
+ if (this.lastHeldItem.name !== block?.name) {
312
+ return true
313
+ }
314
+ // eslint-disable-next-line sonarjs/prefer-single-boolean-return
315
+ if (!Item.equal(this.lastHeldItem.fullItem, block?.fullItem ?? {}) || JSON.stringify(this.lastHeldItem.fullItem.components) !== JSON.stringify(block?.fullItem?.components)) {
316
+ return true
317
+ }
318
+
319
+ return false
320
+ }
321
+
322
+ updateCameraGroup() {
323
+ if (this.stopUpdate) return
324
+ const { camera } = this
325
+ this.cameraGroup.position.copy(camera.position)
326
+ this.cameraGroup.rotation.copy(camera.rotation)
327
+
328
+ // const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
329
+ // const aspect = viewerSize.width / viewerSize.height
330
+ const aspect = 1
331
+
332
+
333
+ // Adjust the position based on the aspect ratio
334
+ const { position, scale: scaleData } = this.getHandHeld3d()
335
+ const distance = -position.z
336
+ const side = this.offHandModeLegacy ? -1 : 1
337
+ this.objectOuterGroup.position.set(
338
+ distance * position.x * aspect * side,
339
+ distance * position.y,
340
+ -distance
341
+ )
342
+
343
+ // const scale = Math.min(0.8, Math.max(1, 1 * aspect))
344
+ const scale = scaleData * 2.22 * 0.2
345
+ this.objectOuterGroup.scale.set(scale, scale, scale)
346
+ }
347
+
348
+ lastItemModelName: string | undefined
349
+ private async createItemModel(handItem: HandItemBlock): Promise<{ model: THREE.Object3D; type: 'hand' | 'block' | 'item' } | undefined> {
350
+ this.lastUpdate = performance.now()
351
+ if (!handItem || (handItem.type === 'hand' && !this.playerHand)) return undefined
352
+
353
+ let blockInner: THREE.Object3D | undefined
354
+ if (handItem.type === 'item' || handItem.type === 'block') {
355
+ const result = this.worldRenderer.entities.getItemMesh({
356
+ ...handItem.fullItem,
357
+ itemId: handItem.id,
358
+ }, {
359
+ 'minecraft:display_context': 'firstperson',
360
+ 'minecraft:use_duration': this.worldRenderer.playerStateReactive.itemUsageTicks,
361
+ 'minecraft:using_item': !!this.worldRenderer.playerStateReactive.itemUsageTicks,
362
+ }, false, this.lastItemModelName)
363
+ if (result) {
364
+ const { mesh: itemMesh, isBlock, modelName } = result
365
+ if (isBlock) {
366
+ blockInner = itemMesh
367
+ handItem.type = 'block'
368
+ } else {
369
+ itemMesh.position.set(0.5, 0.5, 0.5)
370
+ blockInner = itemMesh
371
+ handItem.type = 'item'
372
+ }
373
+ this.lastItemModelName = modelName
374
+ }
375
+ } else {
376
+ blockInner = this.playerHand!
377
+ }
378
+ if (!blockInner) return
379
+ blockInner.name = 'holdingBlock'
380
+
381
+ const rotationDeg = this.getHandHeld3d().rotation
382
+ blockInner.rotation.x = THREE.MathUtils.degToRad(rotationDeg.x)
383
+ blockInner.rotation.y = THREE.MathUtils.degToRad(rotationDeg.y)
384
+ blockInner.rotation.z = THREE.MathUtils.degToRad(rotationDeg.z)
385
+
386
+ return { model: blockInner, type: handItem.type }
387
+ }
388
+
389
+ async replaceItemModel(handItem?: HandItemBlock): Promise<void> {
390
+ // if switch animation is in progress, do not replace the item
391
+ if (this.blockSwapAnimation?.switcher.isTransitioning) return
392
+
393
+ if (!handItem) {
394
+ this.holdingBlock?.removeFromParent()
395
+ this.holdingBlock = undefined
396
+ this.swingAnimator?.stopSwing()
397
+ this.swingAnimator = undefined
398
+ this.idleAnimator = undefined
399
+ return
400
+ }
401
+
402
+ const result = await this.createItemModel(handItem)
403
+ if (!result) return
404
+
405
+ // Update the model without changing the group structure
406
+ this.holdingBlock?.removeFromParent()
407
+ this.holdingBlock = result.model
408
+ this.holdingBlockInnerGroup.add(result.model)
409
+
410
+
411
+ }
412
+
413
+ testUnknownBlockSwitch() {
414
+ void this.setNewItem({
415
+ type: 'item',
416
+ name: 'minecraft:some-unknown-block',
417
+ id: 0,
418
+ fullItem: {}
419
+ })
420
+ }
421
+
422
+ switchRequest = 0
423
+ async setNewItem(handItem?: HandItemBlock) {
424
+ if (!this.isDifferentItem(handItem)) return
425
+ this.lastItemModelName = undefined
426
+ const switchRequest = ++this.switchRequest
427
+ this.lastHeldItem = handItem
428
+ let playAppearAnimation = false
429
+ if (this.holdingBlock) {
430
+ // play disappear animation
431
+ playAppearAnimation = true
432
+ const result = await this.playBlockSwapAnimation('disappeared')
433
+ if (!result) return
434
+ this.holdingBlock?.removeFromParent()
435
+ this.holdingBlock = undefined
436
+ }
437
+
438
+ if (!handItem) {
439
+ this.swingAnimator?.stopSwing()
440
+ this.swingAnimator = undefined
441
+ this.idleAnimator = undefined
442
+ this.blockSwapAnimation = undefined
443
+ return
444
+ }
445
+
446
+ if (switchRequest !== this.switchRequest) return
447
+ const result = await this.createItemModel(handItem)
448
+ if (!result || switchRequest !== this.switchRequest) return
449
+
450
+ const blockOuterGroup = new THREE.Group()
451
+ this.holdingBlockInnerGroup.removeFromParent()
452
+ this.holdingBlockInnerGroup = new THREE.Group()
453
+ this.holdingBlockInnerGroup.add(result.model)
454
+ blockOuterGroup.add(this.holdingBlockInnerGroup)
455
+ this.holdingBlock = result.model
456
+ this.objectInnerGroup = new THREE.Group()
457
+ this.objectInnerGroup.add(blockOuterGroup)
458
+ this.objectInnerGroup.position.set(-0.5, -0.5, -0.5)
459
+ if (playAppearAnimation) {
460
+ this.objectInnerGroup.position.y -= this.objectInnerGroup.scale.y * 1.5
461
+ }
462
+ Object.assign(blockOuterGroup.position, { x: 0.5, y: 0.5, z: 0.5 })
463
+
464
+ this.objectOuterGroup = new THREE.Group()
465
+ this.objectOuterGroup.add(this.objectInnerGroup)
466
+
467
+ this.cameraGroup.add(this.objectOuterGroup)
468
+ const rotationDeg = this.getHandHeld3d().rotation
469
+ this.objectOuterGroup.rotation.y = THREE.MathUtils.degToRad(rotationDeg.yOuter)
470
+
471
+ if (playAppearAnimation) {
472
+ await this.playBlockSwapAnimation('appeared')
473
+ }
474
+
475
+ this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
476
+ this.swingAnimator.type = result.type
477
+ if (this.config.viewBobbing) {
478
+ this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
479
+ }
480
+ }
481
+
482
+ getHandHeld3d() {
483
+ const type = this.lastHeldItem?.type ?? 'hand'
484
+ const side = this.offHandModeLegacy ? 'Left' : 'Right'
485
+
486
+ let scale = 0.8 * 1.15 // default scale for hand
487
+ let position = {
488
+ x: 0.4,
489
+ y: -0.7,
490
+ z: -0.45
491
+ }
492
+ let rotation = {
493
+ x: -32.4,
494
+ y: 42.8,
495
+ z: -41.3,
496
+ yOuter: 0
497
+ }
498
+
499
+ if (type === 'item') {
500
+ const itemData = rotationPositionData[`item${side}`]
501
+ position = {
502
+ x: -0.05,
503
+ y: -0.7,
504
+ z: -0.45
505
+ }
506
+ rotation = {
507
+ x: itemData.rotation[0],
508
+ y: itemData.rotation[1],
509
+ z: itemData.rotation[2],
510
+ yOuter: 0
511
+ }
512
+ scale = itemData.scale[0] * 1.15
513
+ } else if (type === 'block') {
514
+ const blockData = rotationPositionData[`block${side}`]
515
+ position = {
516
+ x: 0.4,
517
+ y: -0.7,
518
+ z: -0.45
519
+ }
520
+ rotation = {
521
+ x: blockData.rotation[0],
522
+ y: blockData.rotation[1],
523
+ z: blockData.rotation[2],
524
+ yOuter: 0
525
+ }
526
+ scale = blockData.scale[0] * 1.15
527
+ }
528
+
529
+ return {
530
+ rotation,
531
+ position,
532
+ scale
533
+ }
534
+ }
535
+
536
+ dispose() {
537
+ this.disposed = true
538
+ this.ready = false
539
+ this.unsubs.forEach(fn => fn())
540
+ this.unsubs = []
541
+ this.idleAnimator?.destroy()
542
+ this.idleAnimator = undefined
543
+ this.swingAnimator?.stopSwing()
544
+ this.swingAnimator = undefined
545
+ this.blockSwapAnimation?.switcher.forceFinish()
546
+ this.blockSwapAnimation = undefined
547
+ disposeObject(this.cameraGroup, true)
548
+ disposeObject(this.objectOuterGroup, true)
549
+ disposeObject(this.objectInnerGroup, true)
550
+ disposeObject(this.holdingBlockInnerGroup, true)
551
+ this.holdingBlock = undefined
552
+ if (this.playerHand) {
553
+ disposeObject(this.playerHand, true)
554
+ this.playerHand = undefined
555
+ }
556
+ }
557
+ }
558
+
559
+ class HandIdleAnimator {
560
+ globalTime = 0
561
+ lastTime = 0
562
+ currentState: MovementState
563
+ targetState: MovementState
564
+ defaultPosition: { x: number; y: number; z: number; rotationX: number; rotationY: number; rotationZ: number }
565
+ private readonly idleOffset = { y: 0, rotationZ: 0 }
566
+ private readonly tween = new tweenJs.Group()
567
+ private idleTween: tweenJs.Tween<{ y: number; rotationZ: number }> | null = null
568
+ private readonly stateSwitcher: SmoothSwitcher
569
+
570
+ // Debug parameters
571
+ private readonly debugParams = {
572
+ // Transition durations for different state changes
573
+ walkingSpeed: 8,
574
+ sprintingSpeed: 16,
575
+ walkingAmplitude: { x: 1 / 30, y: 1 / 10, rotationZ: 0.25 },
576
+ sprintingAmplitude: { x: 1 / 30, y: 1 / 10, rotationZ: 0.4 }
577
+ }
578
+
579
+ private readonly debugGui: DebugGui
580
+
581
+ constructor(public handMesh: THREE.Object3D, public playerState: PlayerStateRenderer) {
582
+ this.handMesh = handMesh
583
+ this.globalTime = 0
584
+ this.currentState = 'NOT_MOVING'
585
+ this.targetState = 'NOT_MOVING'
586
+
587
+ this.defaultPosition = {
588
+ x: handMesh.position.x,
589
+ y: handMesh.position.y,
590
+ z: handMesh.position.z,
591
+ rotationX: handMesh.rotation.x,
592
+ rotationY: handMesh.rotation.y,
593
+ rotationZ: handMesh.rotation.z
594
+ }
595
+
596
+ // Initialize state switcher with appropriate speeds
597
+ this.stateSwitcher = new SmoothSwitcher(
598
+ () => {
599
+ return {
600
+ x: this.handMesh.position.x,
601
+ y: this.handMesh.position.y,
602
+ z: this.handMesh.position.z,
603
+ rotationX: this.handMesh.rotation.x,
604
+ rotationY: this.handMesh.rotation.y,
605
+ rotationZ: this.handMesh.rotation.z
606
+ }
607
+ },
608
+ (property, value) => {
609
+ switch (property) {
610
+ case 'x': this.handMesh.position.x = value; break
611
+ case 'y': this.handMesh.position.y = value; break
612
+ case 'z': this.handMesh.position.z = value; break
613
+ case 'rotationX': this.handMesh.rotation.x = value; break
614
+ case 'rotationY': this.handMesh.rotation.y = value; break
615
+ case 'rotationZ': this.handMesh.rotation.z = value; break
616
+ }
617
+ },
618
+ {
619
+ x: 2, // units per second
620
+ y: 2,
621
+ z: 2,
622
+ rotation: Math.PI // radians per second
623
+ }
624
+ )
625
+
626
+ // Initialize debug GUI
627
+ this.debugGui = new DebugGui('idle_animator', this.debugParams)
628
+ // this.debugGui.activate()
629
+ }
630
+
631
+ private startIdleAnimation() {
632
+ if (this.idleTween) {
633
+ this.idleTween.stop()
634
+ }
635
+
636
+ // Start from current position for smooth transition
637
+ this.idleOffset.y = this.handMesh.position.y - this.defaultPosition.y
638
+ this.idleOffset.rotationZ = this.handMesh.rotation.z - this.defaultPosition.rotationZ
639
+
640
+ this.idleTween = new tweenJs.Tween(this.idleOffset, this.tween)
641
+ .to({
642
+ y: 0.05,
643
+ rotationZ: 0.05
644
+ }, 3000)
645
+ .easing(tweenJs.Easing.Sinusoidal.InOut)
646
+ .yoyo(true)
647
+ .repeat(Infinity)
648
+ .start()
649
+ }
650
+
651
+ private stopIdleAnimation() {
652
+ if (this.idleTween) {
653
+ this.idleTween.stop()
654
+ this.idleOffset.y = 0
655
+ this.idleOffset.rotationZ = 0
656
+ }
657
+ }
658
+
659
+ private getStateTransform(state: MovementState, time: number) {
660
+ switch (state) {
661
+ case 'NOT_MOVING':
662
+ case 'SNEAKING':
663
+ return {
664
+ x: this.defaultPosition.x,
665
+ y: this.defaultPosition.y,
666
+ z: this.defaultPosition.z,
667
+ rotationX: this.defaultPosition.rotationX,
668
+ rotationY: this.defaultPosition.rotationY,
669
+ rotationZ: this.defaultPosition.rotationZ
670
+ }
671
+ case 'WALKING':
672
+ case 'SPRINTING': {
673
+ const speed = state === 'SPRINTING' ? this.debugParams.sprintingSpeed : this.debugParams.walkingSpeed
674
+ const amplitude = state === 'SPRINTING' ? this.debugParams.sprintingAmplitude : this.debugParams.walkingAmplitude
675
+
676
+ return {
677
+ x: this.defaultPosition.x + Math.sin(time * speed) * amplitude.x,
678
+ y: this.defaultPosition.y - Math.abs(Math.cos(time * speed)) * amplitude.y,
679
+ z: this.defaultPosition.z,
680
+ rotationX: this.defaultPosition.rotationX,
681
+ rotationY: this.defaultPosition.rotationY,
682
+ // rotationZ: this.defaultPosition.rotationZ + Math.sin(time * speed) * amplitude.rotationZ
683
+ rotationZ: this.defaultPosition.rotationZ
684
+ }
685
+ }
686
+ }
687
+ }
688
+
689
+ setState(newState: MovementState) {
690
+ if (newState === this.targetState) return
691
+
692
+ this.targetState = newState
693
+ const noTransition = false
694
+ if (this.currentState !== newState) {
695
+ // Stop idle animation during state transitions
696
+ this.stopIdleAnimation()
697
+
698
+ // Calculate new state transform
699
+ if (!noTransition) {
700
+ // this.globalTime = 0
701
+ const stateTransform = this.getStateTransform(newState, this.globalTime)
702
+
703
+ // Start transition to new state
704
+ this.stateSwitcher.transitionTo(stateTransform, newState)
705
+ // this.updated = false
706
+ }
707
+ this.currentState = newState
708
+ }
709
+ }
710
+
711
+ updated = false
712
+ update() {
713
+ this.stateSwitcher.update()
714
+
715
+ const now = performance.now()
716
+ const deltaTime = (now - this.lastTime) / 1000
717
+ this.lastTime = now
718
+
719
+ // Update global time based on current state
720
+ if (!this.stateSwitcher.isTransitioning) {
721
+ switch (this.currentState) {
722
+ case 'NOT_MOVING':
723
+ case 'SNEAKING':
724
+ this.globalTime = Math.PI / 4
725
+ break
726
+ case 'SPRINTING':
727
+ case 'WALKING':
728
+ this.globalTime += deltaTime
729
+ break
730
+ }
731
+ }
732
+
733
+ // Check for state changes from player state
734
+ if (this.playerState) {
735
+ const newState = this.playerState.movementState
736
+ if (newState !== this.targetState) {
737
+ this.setState(newState)
738
+ }
739
+ }
740
+
741
+ // If we're not transitioning between states and in a stable state that should have idle animation
742
+ if (!this.stateSwitcher.isTransitioning &&
743
+ (this.currentState === 'NOT_MOVING' || this.currentState === 'SNEAKING')) {
744
+ // Start idle animation if not already running
745
+ if (!this.idleTween?.isPlaying()) {
746
+ this.startIdleAnimation()
747
+ }
748
+ // Update idle animation
749
+ this.tween.update()
750
+
751
+ // Apply idle offsets
752
+ this.handMesh.position.y = this.defaultPosition.y + this.idleOffset.y
753
+ this.handMesh.rotation.z = this.defaultPosition.rotationZ + this.idleOffset.rotationZ
754
+ }
755
+
756
+ // If we're in a movement state and not transitioning, update the movement animation
757
+ if (!this.stateSwitcher.isTransitioning &&
758
+ (this.currentState === 'WALKING' || this.currentState === 'SPRINTING')) {
759
+ const stateTransform = this.getStateTransform(this.currentState, this.globalTime)
760
+ Object.assign(this.handMesh.position, stateTransform)
761
+ Object.assign(this.handMesh.rotation, {
762
+ x: stateTransform.rotationX,
763
+ y: stateTransform.rotationY,
764
+ z: stateTransform.rotationZ
765
+ })
766
+ // this.stateSwitcher.transitionTo(stateTransform, this.currentState)
767
+ }
768
+ }
769
+
770
+ getCurrentState() {
771
+ return this.currentState
772
+ }
773
+
774
+ destroy() {
775
+ this.stopIdleAnimation()
776
+ this.stateSwitcher.forceFinish()
777
+ }
778
+ }
779
+
780
+ class HandSwingAnimator {
781
+ private readonly PI = Math.PI
782
+ private animationTimer = 0
783
+ private lastTime = 0
784
+ private isAnimating = false
785
+ private stopRequested = false
786
+ private readonly originalRotation: THREE.Euler
787
+ private readonly originalPosition: THREE.Vector3
788
+ private readonly originalScale: THREE.Vector3
789
+
790
+ readonly debugParams = {
791
+ // Animation timing
792
+ animationTime: 250,
793
+ animationStage: 0,
794
+ useClassicSwing: true,
795
+
796
+ // Item/Block animation parameters
797
+ itemSwingXPosScale: -0.8,
798
+ itemSwingYPosScale: 0.2,
799
+ itemSwingZPosScale: -0.2,
800
+ itemHeightScale: -0.6,
801
+ itemPreswingRotY: 45,
802
+ itemSwingXRotAmount: -30,
803
+ itemSwingYRotAmount: -35,
804
+ itemSwingZRotAmount: -5,
805
+
806
+ // Hand/Arm animation parameters
807
+ armSwingXPosScale: -0.3,
808
+ armSwingYPosScale: 0.4,
809
+ armSwingZPosScale: -0.4,
810
+ armSwingYRotAmount: 70,
811
+ armSwingZRotAmount: -20,
812
+ armHeightScale: -0.6
813
+ }
814
+
815
+ private readonly debugGui: DebugGui
816
+
817
+ public type: 'hand' | 'block' | 'item' = 'hand'
818
+
819
+ constructor(public handMesh: THREE.Object3D) {
820
+ this.handMesh = handMesh
821
+ // Store initial transforms
822
+ this.originalRotation = handMesh.rotation.clone()
823
+ this.originalPosition = handMesh.position.clone()
824
+ this.originalScale = handMesh.scale.clone()
825
+
826
+ // Initialize debug GUI
827
+ this.debugGui = new DebugGui('hand_animator', this.debugParams, undefined, {
828
+ animationStage: {
829
+ min: 0,
830
+ max: 1,
831
+ step: 0.01
832
+ },
833
+ // Add ranges for all animation parameters
834
+ itemSwingXPosScale: { min: -2, max: 2, step: 0.1 },
835
+ itemSwingYPosScale: { min: -2, max: 2, step: 0.1 },
836
+ itemSwingZPosScale: { min: -2, max: 2, step: 0.1 },
837
+ itemHeightScale: { min: -2, max: 2, step: 0.1 },
838
+ itemPreswingRotY: { min: -180, max: 180, step: 5 },
839
+ itemSwingXRotAmount: { min: -180, max: 180, step: 5 },
840
+ itemSwingYRotAmount: { min: -180, max: 180, step: 5 },
841
+ itemSwingZRotAmount: { min: -180, max: 180, step: 5 },
842
+ armSwingXPosScale: { min: -2, max: 2, step: 0.1 },
843
+ armSwingYPosScale: { min: -2, max: 2, step: 0.1 },
844
+ armSwingZPosScale: { min: -2, max: 2, step: 0.1 },
845
+ armSwingYRotAmount: { min: -180, max: 180, step: 5 },
846
+ armSwingZRotAmount: { min: -180, max: 180, step: 5 },
847
+ armHeightScale: { min: -2, max: 2, step: 0.1 }
848
+ })
849
+ // this.debugGui.activate()
850
+ }
851
+
852
+ update() {
853
+ if (!this.isAnimating && !this.debugParams.animationStage) {
854
+ // If not animating, ensure we're at original position
855
+ this.handMesh.rotation.copy(this.originalRotation)
856
+ this.handMesh.position.copy(this.originalPosition)
857
+ this.handMesh.scale.copy(this.originalScale)
858
+ return
859
+ }
860
+
861
+ const now = performance.now()
862
+ const deltaTime = (now - this.lastTime) / 1000
863
+ this.lastTime = now
864
+
865
+ // Update animation progress
866
+ this.animationTimer += deltaTime * 1000 // Convert to ms
867
+
868
+ // Calculate animation stage (0 to 1)
869
+ const stage = this.debugParams.animationStage || Math.min(this.animationTimer / this.debugParams.animationTime, 1)
870
+
871
+ if (stage >= 1) {
872
+ // Animation complete
873
+ if (this.stopRequested) {
874
+ // If stop was requested, actually stop now that we've completed a swing
875
+ this.isAnimating = false
876
+ this.stopRequested = false
877
+ this.animationTimer = 0
878
+ this.handMesh.rotation.copy(this.originalRotation)
879
+ this.handMesh.position.copy(this.originalPosition)
880
+ this.handMesh.scale.copy(this.originalScale)
881
+ return
882
+ }
883
+ // Otherwise reset timer and continue
884
+ this.animationTimer = 0
885
+ return
886
+ }
887
+
888
+ // Start from original transforms
889
+ this.handMesh.rotation.copy(this.originalRotation)
890
+ this.handMesh.position.copy(this.originalPosition)
891
+ this.handMesh.scale.copy(this.originalScale)
892
+
893
+ // Calculate swing progress
894
+ const swingProgress = stage
895
+ const sqrtProgress = Math.sqrt(swingProgress)
896
+ const sinProgress = Math.sin(swingProgress * this.PI)
897
+ const sinSqrtProgress = Math.sin(sqrtProgress * this.PI)
898
+
899
+ if (this.type === 'hand') {
900
+ // Hand animation
901
+ const xOffset = this.debugParams.armSwingXPosScale * sinSqrtProgress
902
+ const yOffset = this.debugParams.armSwingYPosScale * Math.sin(sqrtProgress * this.PI * 2)
903
+ const zOffset = this.debugParams.armSwingZPosScale * sinProgress
904
+
905
+ this.handMesh.position.x += xOffset
906
+ this.handMesh.position.y += yOffset + this.debugParams.armHeightScale * swingProgress
907
+ this.handMesh.position.z += zOffset
908
+
909
+ // Rotations
910
+ this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.armSwingYRotAmount * sinSqrtProgress)
911
+ this.handMesh.rotation.z += THREE.MathUtils.degToRad(this.debugParams.armSwingZRotAmount * sinProgress)
912
+ } else {
913
+ // Item/Block animation
914
+ const xOffset = this.debugParams.itemSwingXPosScale * sinSqrtProgress
915
+ const yOffset = this.debugParams.itemSwingYPosScale * Math.sin(sqrtProgress * this.PI * 2)
916
+ const zOffset = this.debugParams.itemSwingZPosScale * sinProgress
917
+
918
+ this.handMesh.position.x += xOffset
919
+ this.handMesh.position.y += yOffset + this.debugParams.itemHeightScale * swingProgress
920
+ this.handMesh.position.z += zOffset
921
+
922
+ // Pre-swing rotation
923
+ this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.itemPreswingRotY)
924
+
925
+ // Swing rotations
926
+ this.handMesh.rotation.x += THREE.MathUtils.degToRad(this.debugParams.itemSwingXRotAmount * sinProgress)
927
+ this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.itemSwingYRotAmount * sinSqrtProgress)
928
+ this.handMesh.rotation.z += THREE.MathUtils.degToRad(this.debugParams.itemSwingZRotAmount * sinProgress)
929
+ }
930
+ }
931
+
932
+ startSwing() {
933
+ this.stopRequested = false
934
+ if (this.isAnimating) return
935
+
936
+ this.isAnimating = true
937
+ this.animationTimer = 0
938
+ this.lastTime = performance.now()
939
+ }
940
+
941
+ stopSwing() {
942
+ if (!this.isAnimating) return
943
+ this.stopRequested = true
944
+ }
945
+
946
+ isCurrentlySwinging() {
947
+ return this.isAnimating
948
+ }
949
+ }
950
+
951
+ export const getBlockMeshFromModel = (material: THREE.Material, model: BlockModel, name: string, blockProvider: WorldBlockProvider, mcData: IndexedData) => {
952
+ const worldRenderModel = blockProvider.transformModel(model, {
953
+ name,
954
+ properties: {}
955
+ }) as any
956
+ return getThreeBlockModelGroup(material, [[worldRenderModel]], undefined, 'plains', mcData)
957
+ }