minecraft-renderer 0.1.30 → 0.1.32

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.
@@ -2,7 +2,7 @@
2
2
  import * as THREE from 'three'
3
3
  import * as tweenJs from '@tweenjs/tween.js'
4
4
  import PrismarineItem from 'prismarine-item'
5
- import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
5
+ import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
6
6
  import { BlockModel } from 'mc-assets'
7
7
  import { DebugGui } from '../lib/DebugGui'
8
8
  import { SmoothSwitcher } from '../lib/smoothSwitcher'
@@ -10,85 +10,78 @@ import { watchProperty } from '../lib/utils/proxy'
10
10
  import { getMyHand } from './hand'
11
11
  import { WorldRendererThree } from './worldRendererThree'
12
12
  import { disposeObject } from './threeJsUtils'
13
+ import type { IHoldingBlock } from './holdingBlockTypes'
13
14
  import { HandItemBlock, MovementState } from '../playerState/types'
14
15
  import { PlayerStateRenderer } from '../playerState/playerState'
15
16
  import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
16
17
  import { IndexedData } from 'minecraft-data'
17
- import type { ResourcesManagerTransferred } from '../resourcesManager'
18
18
  import { WorldRendererConfig } from '../graphicsBackend'
19
+ import { computeCameraBob, type CameraBobInput } from '../lib/cameraBobbing'
20
+
21
+ const _tempMat = new THREE.Matrix4()
22
+
23
+ // Vanilla renderPlayerArm transform chain
24
+ function buildBareHandMatrix(swingProgress: number, equipProgress: number): THREE.Matrix4 {
25
+ const mat = new THREE.Matrix4()
26
+ const side = 1 // right hand
27
+
28
+ const sqrtSwing = Math.sqrt(swingProgress)
29
+ const swingX = -0.3 * Math.sin(sqrtSwing * Math.PI)
30
+ const swingY = 0.4 * Math.sin(sqrtSwing * 2 * Math.PI)
31
+ const swingZ = -0.4 * Math.sin(swingProgress * Math.PI)
32
+
33
+ // Step 1: Base position with swing
34
+ mat.multiply(_tempMat.makeTranslation(side * (swingX + 0.64), swingY - 0.6 + equipProgress * -0.6, swingZ - 0.72))
35
+ // Step 2: Base Y rotation 45°
36
+ mat.multiply(_tempMat.makeRotationY(side * 45 * Math.PI / 180))
37
+ // Step 3: Swing Y rotation
38
+ mat.multiply(_tempMat.makeRotationY(side * Math.sin(sqrtSwing * Math.PI) * 70 * Math.PI / 180))
39
+ // Step 4: Swing Z rotation
40
+ mat.multiply(_tempMat.makeRotationZ(side * Math.sin(swingProgress * swingProgress * Math.PI) * -20 * Math.PI / 180))
41
+ // Step 5: Second translation
42
+ mat.multiply(_tempMat.makeTranslation(side * -1, 3.6, 3.5))
43
+ // Step 6: Z rotation 120°
44
+ mat.multiply(_tempMat.makeRotationZ(side * 120 * Math.PI / 180))
45
+ // Step 7: X rotation 200°
46
+ mat.multiply(_tempMat.makeRotationX(200 * Math.PI / 180))
47
+ // Step 8: Y rotation -135°
48
+ mat.multiply(_tempMat.makeRotationY(side * -135 * Math.PI / 180))
49
+ // Step 9: Final X offset
50
+ mat.multiply(_tempMat.makeTranslation(side * 5.6, 0, 0))
51
+ // Step 10: translateToHand - arm part position (-5/16, 2/16, 0)
52
+ mat.multiply(_tempMat.makeTranslation(side * -5 / 16, 2 / 16, 0))
53
+
54
+ return mat
55
+ }
19
56
 
20
- const rotationPositionData = {
21
- itemRight: {
22
- 'rotation': [
23
- 0,
24
- -90,
25
- 25
26
- ],
27
- 'translation': [
28
- 1.13,
29
- 3.2,
30
- 1.13
31
- ],
32
- 'scale': [
33
- 0.68,
34
- 0.68,
35
- 0.68
36
- ]
37
- },
38
- itemLeft: {
39
- 'rotation': [
40
- 0,
41
- 90,
42
- -25
43
- ],
44
- 'translation': [
45
- 1.13,
46
- 3.2,
47
- 1.13
48
- ],
49
- 'scale': [
50
- 0.68,
51
- 0.68,
52
- 0.68
53
- ]
54
- },
55
- blockRight: {
56
- 'rotation': [
57
- 0,
58
- 45,
59
- 0
60
- ],
61
- 'translation': [
62
- 0,
63
- 0,
64
- 0
65
- ],
66
- 'scale': [
67
- 0.4,
68
- 0.4,
69
- 0.4
70
- ]
71
- },
72
- blockLeft: {
73
- 'rotation': [
74
- 0,
75
- 225,
76
- 0
77
- ],
78
- 'translation': [
79
- 0,
80
- 0,
81
- 0
82
- ],
83
- 'scale': [
84
- 0.4,
85
- 0.4,
86
- 0.4
87
- ]
88
- }
57
+ // Vanilla item arm transforms: applyItemArmTransform + applyItemArmAttackTransform
58
+ function buildItemArmMatrix(swingProgress: number, equipProgress: number): THREE.Matrix4 {
59
+ const mat = new THREE.Matrix4()
60
+ const side = 1 // right hand
61
+
62
+ const sqrtSwing = Math.sqrt(swingProgress)
63
+
64
+ // Swing position offsets (from renderArmWithItem default branch)
65
+ const swingX = -0.4 * Math.sin(sqrtSwing * Math.PI)
66
+ const swingY = 0.2 * Math.sin(sqrtSwing * 2 * Math.PI)
67
+ const swingZ = -0.2 * Math.sin(swingProgress * Math.PI)
68
+ mat.multiply(_tempMat.makeTranslation(side * swingX, swingY, swingZ))
69
+
70
+ // applyItemArmTransform: translate(±0.56, -0.52 + equip*-0.6, -0.72)
71
+ mat.multiply(_tempMat.makeTranslation(side * 0.56, -0.52 + equipProgress * -0.6, -0.72))
72
+
73
+ // applyItemArmAttackTransform
74
+ const sinSwingSq = Math.sin(swingProgress * swingProgress * Math.PI)
75
+ const sinSqrtSwing = Math.sin(sqrtSwing * Math.PI)
76
+ mat.multiply(_tempMat.makeRotationY(side * (45 + sinSwingSq * -20) * Math.PI / 180))
77
+ mat.multiply(_tempMat.makeRotationZ(side * sinSqrtSwing * -20 * Math.PI / 180))
78
+ mat.multiply(_tempMat.makeRotationX(sinSqrtSwing * -80 * Math.PI / 180))
79
+ mat.multiply(_tempMat.makeRotationY(side * -45 * Math.PI / 180))
80
+
81
+ return mat
89
82
  }
90
83
 
91
- export default class HoldingBlock {
84
+ export default class HoldingBlock implements IHoldingBlock {
92
85
  // TODO refactor with the tree builder for better visual understanding
93
86
  holdingBlock: THREE.Object3D | undefined = undefined
94
87
  blockSwapAnimation: {
@@ -96,36 +89,45 @@ export default class HoldingBlock {
96
89
  // hidden: boolean
97
90
  } | undefined = undefined
98
91
  cameraGroup = new THREE.Mesh()
99
- objectOuterGroup = new THREE.Group() // 3
100
- objectInnerGroup = new THREE.Group() // 4
101
- holdingBlockInnerGroup = new THREE.Group() // 5
102
- camera = new THREE.PerspectiveCamera(75, 1, 0.1, 100)
92
+ armTransformGroup = new THREE.Group()
93
+ camera = new THREE.PerspectiveCamera(70, 1, 0.05, 100)
94
+ equipProgress = 0 // 0 = fully visible, 1 = hidden
103
95
  stopUpdate = false
104
96
  lastHeldItem: HandItemBlock | undefined
97
+ currentDisplayType: 'hand' | 'item' | 'block' = 'hand'
105
98
  isSwinging = false
106
99
  nextIterStopCallbacks: Array<() => void> | undefined
107
100
  idleAnimator: HandIdleAnimator | undefined
108
101
  ready = false
109
102
  lastUpdate = 0
103
+ xBob = 0
104
+ yBob = 0
105
+ lastBobUpdateTime = 0
106
+ private lastBobWalkDist = 0
107
+ private lastBobTickTime = 0
110
108
  playerHand: THREE.Object3D | undefined
111
109
  offHandDisplay = false
112
110
  offHandModeLegacy = false
113
111
 
114
112
  swingAnimator: HandSwingAnimator | undefined
115
113
  config: WorldRendererConfig
114
+ private disposed = false
115
+ private unsubs: Array<() => void> = []
116
116
 
117
117
  constructor(public worldRenderer: WorldRendererThree, public offHand = false) {
118
118
  this.initCameraGroup()
119
- this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
120
- if (!this.offHand) {
121
- this.updateItem()
122
- }
123
- }, false)
124
- this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => {
125
- if (this.offHand) {
126
- this.updateItem()
127
- }
128
- }, false)
119
+ this.unsubs.push(
120
+ this.worldRenderer.onReactivePlayerStateUpdated('heldItemMain', () => {
121
+ if (!this.offHand) {
122
+ this.updateItem()
123
+ }
124
+ }, false),
125
+ this.worldRenderer.onReactivePlayerStateUpdated('heldItemOff', () => {
126
+ if (this.offHand) {
127
+ this.updateItem()
128
+ }
129
+ }, false)
130
+ )
129
131
  this.config = worldRenderer.displayOptions.inWorldRenderingConfig
130
132
 
131
133
  this.offHandDisplay = this.offHand
@@ -133,12 +135,14 @@ export default class HoldingBlock {
133
135
  if (!this.offHand) {
134
136
  // load default hand
135
137
  void getMyHand().then((hand) => {
138
+ if (this.disposed) return
136
139
  this.playerHand = hand
137
140
  // trigger update
138
141
  this.updateItem()
139
142
  }).then(() => {
143
+ if (this.disposed) return
140
144
  // now watch over the player skin
141
- watchProperty(
145
+ const unsub = watchProperty(
142
146
  async () => {
143
147
  return getMyHand(this.worldRenderer.playerStateReactive.playerSkin, this.worldRenderer.playerStateReactive.onlineMode ? this.worldRenderer.playerStateReactive.username : undefined)
144
148
  },
@@ -155,10 +159,33 @@ export default class HoldingBlock {
155
159
  disposeObject(oldHand!, true)
156
160
  }
157
161
  )
162
+ this.unsubs.push(unsub)
158
163
  })
159
164
  }
160
165
  }
161
166
 
167
+ dispose() {
168
+ this.disposed = true
169
+ this.unsubs.forEach(fn => fn())
170
+ this.unsubs = []
171
+ this.idleAnimator?.destroy()
172
+ this.idleAnimator = undefined
173
+ this.swingAnimator?.stopSwing()
174
+ this.swingAnimator = undefined
175
+ this.blockSwapAnimation?.switcher.forceFinish()
176
+ this.blockSwapAnimation = undefined
177
+ disposeObject(this.cameraGroup, true)
178
+ if (this.holdingBlock && this.holdingBlock !== this.playerHand) {
179
+ disposeObject(this.holdingBlock, true)
180
+ }
181
+ this.holdingBlock = undefined
182
+ if (this.playerHand) {
183
+ disposeObject(this.playerHand, true)
184
+ this.playerHand = undefined
185
+ }
186
+ this.ready = false
187
+ }
188
+
162
189
  updateItem() {
163
190
  if (!this.ready) return
164
191
  const item = this.offHand ? this.worldRenderer.playerStateReactive.heldItemOff : this.worldRenderer.playerStateReactive.heldItemMain
@@ -175,6 +202,9 @@ export default class HoldingBlock {
175
202
 
176
203
  initCameraGroup() {
177
204
  this.cameraGroup = new THREE.Mesh()
205
+ this.armTransformGroup = new THREE.Group()
206
+ this.armTransformGroup.matrixAutoUpdate = false
207
+ this.cameraGroup.add(this.armTransformGroup)
178
208
  }
179
209
 
180
210
  startSwing() {
@@ -192,29 +222,37 @@ export default class HoldingBlock {
192
222
  void this.replaceItemModel(this.lastHeldItem)
193
223
  }
194
224
 
195
- // Only update idle animation if not swinging
225
+ // Only update swing animation
196
226
  if (this.swingAnimator?.isCurrentlySwinging() || this.swingAnimator?.debugParams.animationStage) {
197
227
  this.swingAnimator?.update()
198
- } else {
199
- this.idleAnimator?.update()
200
228
  }
229
+ // Idle animation disabled temporarily
201
230
 
202
231
  this.blockSwapAnimation?.switcher.update()
203
232
 
204
233
  const scene = new THREE.Scene()
205
234
  scene.add(this.cameraGroup)
206
- // if (this.camera.aspect !== originalCamera.aspect) {
207
- // this.camera.aspect = originalCamera.aspect
208
- // this.camera.updateProjectionMatrix()
209
- // }
235
+ const viewerSize = renderer.getSize(new THREE.Vector2())
236
+ const isPortrait = viewerSize.height > viewerSize.width
237
+
238
+ if (isPortrait) {
239
+ // Portrait: use fixed 1:1 aspect with square viewport to keep hand visible
240
+ if (this.camera.aspect !== 1) {
241
+ this.camera.aspect = 1
242
+ this.camera.updateProjectionMatrix()
243
+ }
244
+ } else {
245
+ // Landscape: sync aspect with main camera (vanilla full-viewport rendering)
246
+ if (this.camera.aspect !== originalCamera.aspect) {
247
+ this.camera.aspect = originalCamera.aspect
248
+ this.camera.updateProjectionMatrix()
249
+ }
250
+ }
251
+
210
252
  this.updateCameraGroup()
211
253
  scene.add(ambientLight.clone())
212
254
  scene.add(directionalLight.clone())
213
255
 
214
- const viewerSize = renderer.getSize(new THREE.Vector2())
215
- const minSize = Math.min(viewerSize.width, viewerSize.height)
216
- const x = viewerSize.width - minSize
217
-
218
256
  // Mirror the scene for offhand by scaling
219
257
  const { offHandDisplay } = this
220
258
  if (offHandDisplay) {
@@ -223,15 +261,23 @@ export default class HoldingBlock {
223
261
 
224
262
  renderer.autoClear = false
225
263
  renderer.clearDepth()
226
- if (this.offHandDisplay) {
227
- renderer.setViewport(0, 0, minSize, minSize)
228
- } else {
229
- const x = viewerSize.width - minSize
230
- // if (x) x -= x / 4
231
- renderer.setViewport(x, 0, minSize, minSize)
264
+
265
+ if (isPortrait) {
266
+ // Portrait: render in square viewport anchored to bottom-right (or bottom-left for offhand)
267
+ const minSize = Math.min(viewerSize.width, viewerSize.height)
268
+ if (offHandDisplay) {
269
+ renderer.setViewport(0, 0, minSize, minSize)
270
+ } else {
271
+ renderer.setViewport(viewerSize.width - minSize, 0, minSize, minSize)
272
+ }
232
273
  }
274
+
233
275
  renderer.render(scene, this.camera)
234
- renderer.setViewport(0, 0, viewerSize.width, viewerSize.height)
276
+
277
+ if (isPortrait) {
278
+ // Restore full viewport
279
+ renderer.setViewport(0, 0, viewerSize.width, viewerSize.height)
280
+ }
235
281
 
236
282
  // Reset the mirroring after rendering
237
283
  if (offHandDisplay) {
@@ -257,37 +303,25 @@ export default class HoldingBlock {
257
303
  this.blockSwapAnimation ??= {
258
304
  switcher: new SmoothSwitcher(
259
305
  () => ({
260
- y: this.objectInnerGroup.position.y
306
+ progress: this.equipProgress
261
307
  }),
262
308
  (property, value) => {
263
- if (property === 'y') this.objectInnerGroup.position.y = value
309
+ if (property === 'progress') this.equipProgress = value
264
310
  },
265
311
  {
266
- y: 16 // units per second
312
+ progress: 4 // speed: units per second
267
313
  }
268
314
  )
269
315
  }
270
316
 
271
- const newState = forceState
272
- // if (forceState && newState !== forceState) {
273
- // throw new Error(`forceState does not match current state ${forceState} !== ${newState}`)
274
- // }
275
-
276
- const targetY = this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (newState === 'appeared' ? 1 : -1))
277
-
278
- // if (newState === this.blockSwapAnimation.switcher.transitioningToStateName) {
279
- // return false
280
- // }
281
-
317
+ const targetProgress = forceState === 'disappeared' ? 1 : 0
282
318
  let cancelled = false
283
319
  return new Promise<boolean>((resolve) => {
284
320
  this.blockSwapAnimation!.switcher.transitionTo(
285
- { y: targetY },
286
- newState,
321
+ { progress: targetProgress },
322
+ forceState,
287
323
  () => {
288
- if (!cancelled) {
289
- resolve(true)
290
- }
324
+ if (!cancelled) resolve(true)
291
325
  },
292
326
  () => {
293
327
  cancelled = true
@@ -316,27 +350,74 @@ export default class HoldingBlock {
316
350
  updateCameraGroup() {
317
351
  if (this.stopUpdate) return
318
352
  const { camera } = this
353
+
354
+ // Hand rotation momentum (xBob/yBob) — vanilla Minecraft inertia effect
355
+ // Use base rotation from CameraShake (actual player view angles)
356
+ const now = performance.now()
357
+ const baseRotation = this.worldRenderer.cameraShake.getBaseRotation()
358
+ const actualPitch = baseRotation.pitch
359
+ const actualYaw = baseRotation.yaw
360
+ if (this.lastBobUpdateTime === 0) {
361
+ this.xBob = actualPitch
362
+ this.yBob = actualYaw
363
+ this.lastBobUpdateTime = now
364
+ } else {
365
+ const dt = Math.min((now - this.lastBobUpdateTime) / 1000, 0.1)
366
+ this.lastBobUpdateTime = now
367
+ const factor = 1 - Math.pow(0.5, dt * 20)
368
+ this.xBob += (actualPitch - this.xBob) * factor
369
+ this.yBob += (actualYaw - this.yBob) * factor
370
+ }
371
+ const pitchOffset = (actualPitch - this.xBob) * -0.1
372
+ const yawOffset = (actualYaw - this.yBob) * -0.1
373
+
319
374
  this.cameraGroup.position.copy(camera.position)
320
375
  this.cameraGroup.rotation.copy(camera.rotation)
321
376
 
322
- // const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
323
- // const aspect = viewerSize.width / viewerSize.height
324
- const aspect = 1
377
+ // ─── bobView: Walking hand bob (vanilla-accurate) ───
378
+ const viewBobbing = this.worldRenderer.displayOptions.inWorldRenderingConfig.viewBobbing
379
+ if (viewBobbing) {
380
+ const ps = this.worldRenderer.playerStateReactive
325
381
 
382
+ // Track tick timing for partialTick (same approach as CameraBobbingModule)
383
+ if (ps.walkDist !== this.lastBobWalkDist) {
384
+ this.lastBobTickTime = now
385
+ this.lastBobWalkDist = ps.walkDist
386
+ }
387
+ const partialTick = Math.min((now - this.lastBobTickTime) / 50, 1)
388
+
389
+ const bob = computeCameraBob({
390
+ walkDist: ps.walkDist,
391
+ prevWalkDist: ps.prevWalkDist,
392
+ bob: ps.bob,
393
+ prevBob: ps.prevBob,
394
+ partialTick
395
+ })
326
396
 
327
- // Adjust the position based on the aspect ratio
328
- const { position, scale: scaleData } = this.getHandHeld3d()
329
- const distance = -position.z
330
- const side = this.offHandModeLegacy ? -1 : 1
331
- this.objectOuterGroup.position.set(
332
- distance * position.x * aspect * side,
333
- distance * position.y,
334
- -distance
335
- )
397
+ // Apply bobView position (translate)
398
+ this.cameraGroup.position.x += bob.position.x
399
+ this.cameraGroup.position.y += bob.position.y
400
+
401
+ // Apply bobView rotation (roll Z + pitch X)
402
+ this.cameraGroup.rotation.z += bob.rotation.z
403
+ this.cameraGroup.rotation.x += bob.rotation.x
404
+ }
336
405
 
337
- // const scale = Math.min(0.8, Math.max(1, 1 * aspect))
338
- const scale = scaleData * 2.22 * 0.2
339
- this.objectOuterGroup.scale.set(scale, scale, scale)
406
+ this.cameraGroup.rotation.x += pitchOffset
407
+ this.cameraGroup.rotation.y += yawOffset
408
+
409
+ const type = this.currentDisplayType
410
+ const swingProgress = this.swingAnimator?.getSwingProgress() ?? 0
411
+
412
+ let matrix: THREE.Matrix4
413
+ if (type === 'hand') {
414
+ matrix = buildBareHandMatrix(swingProgress, this.equipProgress)
415
+ } else {
416
+ matrix = buildItemArmMatrix(swingProgress, this.equipProgress)
417
+ }
418
+
419
+ this.armTransformGroup.matrix.copy(matrix)
420
+ this.armTransformGroup.matrixWorldNeedsUpdate = true
340
421
  }
341
422
 
342
423
  lastItemModelName: string | undefined
@@ -360,7 +441,6 @@ export default class HoldingBlock {
360
441
  blockInner = itemMesh
361
442
  handItem.type = 'block'
362
443
  } else {
363
- itemMesh.position.set(0.5, 0.5, 0.5)
364
444
  blockInner = itemMesh
365
445
  handItem.type = 'item'
366
446
  }
@@ -370,12 +450,30 @@ export default class HoldingBlock {
370
450
  blockInner = this.playerHand!
371
451
  }
372
452
  if (!blockInner) return
373
- blockInner.name = 'holdingBlock'
374
453
 
375
- const rotationDeg = this.getHandHeld3d().rotation
376
- blockInner.rotation.x = THREE.MathUtils.degToRad(rotationDeg.x)
377
- blockInner.rotation.y = THREE.MathUtils.degToRad(rotationDeg.y)
378
- blockInner.rotation.z = THREE.MathUtils.degToRad(rotationDeg.z)
454
+ // Apply vanilla firstperson_righthand display transforms (ItemTransform.apply)
455
+ // Vanilla order: translate(÷16) → rotateXYZ → scale, then model centering
456
+ if (handItem.type === 'item' || handItem.type === 'block') {
457
+ const displayGroup = new THREE.Group()
458
+ displayGroup.name = 'displayTransform'
459
+
460
+ if (handItem.type === 'item') {
461
+ // Vanilla item/handheld firstperson_righthand defaults
462
+ // Translation pre-divided by 16 per ItemTransform deserialization
463
+ displayGroup.position.set(1.13 / 16, 3.2 / 16, 1.13 / 16)
464
+ displayGroup.rotation.set(0, THREE.MathUtils.degToRad(-90), THREE.MathUtils.degToRad(25), 'XYZ')
465
+ displayGroup.scale.set(0.68, 0.68, 0.68)
466
+ } else {
467
+ // Vanilla block/block firstperson_righthand defaults
468
+ displayGroup.rotation.set(0, THREE.MathUtils.degToRad(45), 0, 'XYZ')
469
+ displayGroup.scale.set(0.4, 0.4, 0.4)
470
+ }
471
+
472
+ displayGroup.add(blockInner)
473
+ blockInner = displayGroup
474
+ }
475
+
476
+ blockInner.name = 'holdingBlock'
379
477
 
380
478
  return { model: blockInner, type: handItem.type }
381
479
  }
@@ -387,6 +485,7 @@ export default class HoldingBlock {
387
485
  if (!handItem) {
388
486
  this.holdingBlock?.removeFromParent()
389
487
  this.holdingBlock = undefined
488
+ this.currentDisplayType = 'hand'
390
489
  this.swingAnimator?.stopSwing()
391
490
  this.swingAnimator = undefined
392
491
  this.idleAnimator = undefined
@@ -399,7 +498,8 @@ export default class HoldingBlock {
399
498
  // Update the model without changing the group structure
400
499
  this.holdingBlock?.removeFromParent()
401
500
  this.holdingBlock = result.model
402
- this.holdingBlockInnerGroup.add(result.model)
501
+ this.currentDisplayType = result.type
502
+ this.armTransformGroup.add(result.model)
403
503
 
404
504
 
405
505
  }
@@ -419,18 +519,21 @@ export default class HoldingBlock {
419
519
  this.lastItemModelName = undefined
420
520
  const switchRequest = ++this.switchRequest
421
521
  this.lastHeldItem = handItem
522
+
422
523
  let playAppearAnimation = false
423
524
  if (this.holdingBlock) {
424
- // play disappear animation
425
525
  playAppearAnimation = true
426
526
  const result = await this.playBlockSwapAnimation('disappeared')
427
527
  if (!result) return
428
- this.holdingBlock?.removeFromParent()
528
+ this.holdingBlock.removeFromParent()
529
+ if (this.holdingBlock !== this.playerHand) {
530
+ disposeObject(this.holdingBlock, false)
531
+ }
429
532
  this.holdingBlock = undefined
430
533
  }
431
534
 
432
535
  if (!handItem) {
433
- this.swingAnimator?.stopSwing()
536
+ this.currentDisplayType = 'hand'
434
537
  this.swingAnimator = undefined
435
538
  this.idleAnimator = undefined
436
539
  this.blockSwapAnimation = undefined
@@ -441,91 +544,20 @@ export default class HoldingBlock {
441
544
  const result = await this.createItemModel(handItem)
442
545
  if (!result || switchRequest !== this.switchRequest) return
443
546
 
444
- const blockOuterGroup = new THREE.Group()
445
- this.holdingBlockInnerGroup.removeFromParent()
446
- this.holdingBlockInnerGroup = new THREE.Group()
447
- this.holdingBlockInnerGroup.add(result.model)
448
- blockOuterGroup.add(this.holdingBlockInnerGroup)
449
547
  this.holdingBlock = result.model
450
- this.objectInnerGroup = new THREE.Group()
451
- this.objectInnerGroup.add(blockOuterGroup)
452
- this.objectInnerGroup.position.set(-0.5, -0.5, -0.5)
453
- if (playAppearAnimation) {
454
- this.objectInnerGroup.position.y -= this.objectInnerGroup.scale.y * 1.5
455
- }
456
- Object.assign(blockOuterGroup.position, { x: 0.5, y: 0.5, z: 0.5 })
457
-
458
- this.objectOuterGroup = new THREE.Group()
459
- this.objectOuterGroup.add(this.objectInnerGroup)
460
-
461
- this.cameraGroup.add(this.objectOuterGroup)
462
- const rotationDeg = this.getHandHeld3d().rotation
463
- this.objectOuterGroup.rotation.y = THREE.MathUtils.degToRad(rotationDeg.yOuter)
548
+ this.currentDisplayType = result.type
549
+ this.armTransformGroup.add(this.holdingBlock)
464
550
 
465
551
  if (playAppearAnimation) {
466
552
  await this.playBlockSwapAnimation('appeared')
467
553
  }
468
554
 
469
- this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
555
+ this.swingAnimator = new HandSwingAnimator()
470
556
  this.swingAnimator.type = result.type
471
- if (this.config.viewBobbing) {
472
- this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
473
- }
557
+ // Idle animation disabled — walking bob is handled by vanilla bobView applied to cameraGroup
558
+ this.idleAnimator = undefined
474
559
  }
475
560
 
476
- getHandHeld3d() {
477
- const type = this.lastHeldItem?.type ?? 'hand'
478
- const side = this.offHandModeLegacy ? 'Left' : 'Right'
479
-
480
- let scale = 0.8 * 1.15 // default scale for hand
481
- let position = {
482
- x: 0.4,
483
- y: -0.7,
484
- z: -0.45
485
- }
486
- let rotation = {
487
- x: -32.4,
488
- y: 42.8,
489
- z: -41.3,
490
- yOuter: 0
491
- }
492
-
493
- if (type === 'item') {
494
- const itemData = rotationPositionData[`item${side}`]
495
- position = {
496
- x: -0.05,
497
- y: -0.7,
498
- z: -0.45
499
- }
500
- rotation = {
501
- x: itemData.rotation[0],
502
- y: itemData.rotation[1],
503
- z: itemData.rotation[2],
504
- yOuter: 0
505
- }
506
- scale = itemData.scale[0] * 1.15
507
- } else if (type === 'block') {
508
- const blockData = rotationPositionData[`block${side}`]
509
- position = {
510
- x: 0.4,
511
- y: -0.7,
512
- z: -0.45
513
- }
514
- rotation = {
515
- x: blockData.rotation[0],
516
- y: blockData.rotation[1],
517
- z: blockData.rotation[2],
518
- yOuter: 0
519
- }
520
- scale = blockData.scale[0] * 1.15
521
- }
522
-
523
- return {
524
- rotation,
525
- position,
526
- scale
527
- }
528
- }
529
561
  }
530
562
 
531
563
  class HandIdleAnimator {
@@ -750,83 +782,29 @@ class HandIdleAnimator {
750
782
  }
751
783
 
752
784
  class HandSwingAnimator {
753
- private readonly PI = Math.PI
754
785
  private animationTimer = 0
755
786
  private lastTime = 0
756
787
  private isAnimating = false
757
788
  private stopRequested = false
758
- private readonly originalRotation: THREE.Euler
759
- private readonly originalPosition: THREE.Vector3
760
- private readonly originalScale: THREE.Vector3
789
+ private swingProgress = 0
790
+ public type: 'hand' | 'block' | 'item' = 'hand'
761
791
 
762
792
  readonly debugParams = {
763
- // Animation timing
764
793
  animationTime: 250,
765
794
  animationStage: 0,
766
- useClassicSwing: true,
767
-
768
- // Item/Block animation parameters
769
- itemSwingXPosScale: -0.8,
770
- itemSwingYPosScale: 0.2,
771
- itemSwingZPosScale: -0.2,
772
- itemHeightScale: -0.6,
773
- itemPreswingRotY: 45,
774
- itemSwingXRotAmount: -30,
775
- itemSwingYRotAmount: -35,
776
- itemSwingZRotAmount: -5,
777
-
778
- // Hand/Arm animation parameters
779
- armSwingXPosScale: -0.3,
780
- armSwingYPosScale: 0.4,
781
- armSwingZPosScale: -0.4,
782
- armSwingYRotAmount: 70,
783
- armSwingZRotAmount: -20,
784
- armHeightScale: -0.6
785
795
  }
786
796
 
787
797
  private readonly debugGui: DebugGui
788
798
 
789
- public type: 'hand' | 'block' | 'item' = 'hand'
790
-
791
- constructor(public handMesh: THREE.Object3D) {
792
- this.handMesh = handMesh
793
- // Store initial transforms
794
- this.originalRotation = handMesh.rotation.clone()
795
- this.originalPosition = handMesh.position.clone()
796
- this.originalScale = handMesh.scale.clone()
797
-
798
- // Initialize debug GUI
799
+ constructor() {
799
800
  this.debugGui = new DebugGui('hand_animator', this.debugParams, undefined, {
800
- animationStage: {
801
- min: 0,
802
- max: 1,
803
- step: 0.01
804
- },
805
- // Add ranges for all animation parameters
806
- itemSwingXPosScale: { min: -2, max: 2, step: 0.1 },
807
- itemSwingYPosScale: { min: -2, max: 2, step: 0.1 },
808
- itemSwingZPosScale: { min: -2, max: 2, step: 0.1 },
809
- itemHeightScale: { min: -2, max: 2, step: 0.1 },
810
- itemPreswingRotY: { min: -180, max: 180, step: 5 },
811
- itemSwingXRotAmount: { min: -180, max: 180, step: 5 },
812
- itemSwingYRotAmount: { min: -180, max: 180, step: 5 },
813
- itemSwingZRotAmount: { min: -180, max: 180, step: 5 },
814
- armSwingXPosScale: { min: -2, max: 2, step: 0.1 },
815
- armSwingYPosScale: { min: -2, max: 2, step: 0.1 },
816
- armSwingZPosScale: { min: -2, max: 2, step: 0.1 },
817
- armSwingYRotAmount: { min: -180, max: 180, step: 5 },
818
- armSwingZRotAmount: { min: -180, max: 180, step: 5 },
819
- armHeightScale: { min: -2, max: 2, step: 0.1 }
801
+ animationStage: { min: 0, max: 1, step: 0.01 },
820
802
  })
821
- // this.debugGui.activate()
822
803
  }
823
804
 
824
805
  update() {
825
806
  if (!this.isAnimating && !this.debugParams.animationStage) {
826
- // If not animating, ensure we're at original position
827
- this.handMesh.rotation.copy(this.originalRotation)
828
- this.handMesh.position.copy(this.originalPosition)
829
- this.handMesh.scale.copy(this.originalScale)
807
+ this.swingProgress = 0
830
808
  return
831
809
  }
832
810
 
@@ -834,77 +812,33 @@ class HandSwingAnimator {
834
812
  const deltaTime = (now - this.lastTime) / 1000
835
813
  this.lastTime = now
836
814
 
837
- // Update animation progress
838
- this.animationTimer += deltaTime * 1000 // Convert to ms
815
+ this.animationTimer += deltaTime * 1000
839
816
 
840
- // Calculate animation stage (0 to 1)
841
817
  const stage = this.debugParams.animationStage || Math.min(this.animationTimer / this.debugParams.animationTime, 1)
842
818
 
843
819
  if (stage >= 1) {
844
- // Animation complete
845
820
  if (this.stopRequested) {
846
- // If stop was requested, actually stop now that we've completed a swing
847
821
  this.isAnimating = false
848
822
  this.stopRequested = false
849
823
  this.animationTimer = 0
850
- this.handMesh.rotation.copy(this.originalRotation)
851
- this.handMesh.position.copy(this.originalPosition)
852
- this.handMesh.scale.copy(this.originalScale)
824
+ this.swingProgress = 0
853
825
  return
854
826
  }
855
- // Otherwise reset timer and continue
856
827
  this.animationTimer = 0
828
+ this.swingProgress = 0
857
829
  return
858
830
  }
859
831
 
860
- // Start from original transforms
861
- this.handMesh.rotation.copy(this.originalRotation)
862
- this.handMesh.position.copy(this.originalPosition)
863
- this.handMesh.scale.copy(this.originalScale)
864
-
865
- // Calculate swing progress
866
- const swingProgress = stage
867
- const sqrtProgress = Math.sqrt(swingProgress)
868
- const sinProgress = Math.sin(swingProgress * this.PI)
869
- const sinSqrtProgress = Math.sin(sqrtProgress * this.PI)
870
-
871
- if (this.type === 'hand') {
872
- // Hand animation
873
- const xOffset = this.debugParams.armSwingXPosScale * sinSqrtProgress
874
- const yOffset = this.debugParams.armSwingYPosScale * Math.sin(sqrtProgress * this.PI * 2)
875
- const zOffset = this.debugParams.armSwingZPosScale * sinProgress
876
-
877
- this.handMesh.position.x += xOffset
878
- this.handMesh.position.y += yOffset + this.debugParams.armHeightScale * swingProgress
879
- this.handMesh.position.z += zOffset
880
-
881
- // Rotations
882
- this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.armSwingYRotAmount * sinSqrtProgress)
883
- this.handMesh.rotation.z += THREE.MathUtils.degToRad(this.debugParams.armSwingZRotAmount * sinProgress)
884
- } else {
885
- // Item/Block animation
886
- const xOffset = this.debugParams.itemSwingXPosScale * sinSqrtProgress
887
- const yOffset = this.debugParams.itemSwingYPosScale * Math.sin(sqrtProgress * this.PI * 2)
888
- const zOffset = this.debugParams.itemSwingZPosScale * sinProgress
889
-
890
- this.handMesh.position.x += xOffset
891
- this.handMesh.position.y += yOffset + this.debugParams.itemHeightScale * swingProgress
892
- this.handMesh.position.z += zOffset
893
-
894
- // Pre-swing rotation
895
- this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.itemPreswingRotY)
832
+ this.swingProgress = stage
833
+ }
896
834
 
897
- // Swing rotations
898
- this.handMesh.rotation.x += THREE.MathUtils.degToRad(this.debugParams.itemSwingXRotAmount * sinProgress)
899
- this.handMesh.rotation.y += THREE.MathUtils.degToRad(this.debugParams.itemSwingYRotAmount * sinSqrtProgress)
900
- this.handMesh.rotation.z += THREE.MathUtils.degToRad(this.debugParams.itemSwingZRotAmount * sinProgress)
901
- }
835
+ getSwingProgress(): number {
836
+ return this.swingProgress
902
837
  }
903
838
 
904
839
  startSwing() {
905
840
  this.stopRequested = false
906
841
  if (this.isAnimating) return
907
-
908
842
  this.isAnimating = true
909
843
  this.animationTimer = 0
910
844
  this.lastTime = performance.now()