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.
@@ -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,75 @@ 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 handBobSpeedMultiplier = 1.8
390
+ const bob = computeCameraBob({
391
+ walkDist: ps.walkDist * handBobSpeedMultiplier,
392
+ prevWalkDist: ps.prevWalkDist * handBobSpeedMultiplier,
393
+ bob: ps.bob,
394
+ prevBob: ps.prevBob,
395
+ partialTick
396
+ })
326
397
 
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
- )
398
+ // Apply bobView position (translate)
399
+ this.cameraGroup.position.x += bob.position.x
400
+ this.cameraGroup.position.y += bob.position.y
401
+
402
+ // Apply bobView rotation (roll Z + pitch X)
403
+ this.cameraGroup.rotation.z += bob.rotation.z
404
+ this.cameraGroup.rotation.x += bob.rotation.x
405
+ }
336
406
 
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)
407
+ this.cameraGroup.rotation.x += pitchOffset
408
+ this.cameraGroup.rotation.y += yawOffset
409
+
410
+ const type = this.currentDisplayType
411
+ const swingProgress = this.swingAnimator?.getSwingProgress() ?? 0
412
+
413
+ let matrix: THREE.Matrix4
414
+ if (type === 'hand') {
415
+ matrix = buildBareHandMatrix(swingProgress, this.equipProgress)
416
+ } else {
417
+ matrix = buildItemArmMatrix(swingProgress, this.equipProgress)
418
+ }
419
+
420
+ this.armTransformGroup.matrix.copy(matrix)
421
+ this.armTransformGroup.matrixWorldNeedsUpdate = true
340
422
  }
341
423
 
342
424
  lastItemModelName: string | undefined
@@ -360,7 +442,6 @@ export default class HoldingBlock {
360
442
  blockInner = itemMesh
361
443
  handItem.type = 'block'
362
444
  } else {
363
- itemMesh.position.set(0.5, 0.5, 0.5)
364
445
  blockInner = itemMesh
365
446
  handItem.type = 'item'
366
447
  }
@@ -370,12 +451,30 @@ export default class HoldingBlock {
370
451
  blockInner = this.playerHand!
371
452
  }
372
453
  if (!blockInner) return
373
- blockInner.name = 'holdingBlock'
374
454
 
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)
455
+ // Apply vanilla firstperson_righthand display transforms (ItemTransform.apply)
456
+ // Vanilla order: translate(÷16) → rotateXYZ → scale, then model centering
457
+ if (handItem.type === 'item' || handItem.type === 'block') {
458
+ const displayGroup = new THREE.Group()
459
+ displayGroup.name = 'displayTransform'
460
+
461
+ if (handItem.type === 'item') {
462
+ // Vanilla item/handheld firstperson_righthand defaults
463
+ // Translation pre-divided by 16 per ItemTransform deserialization
464
+ displayGroup.position.set(1.13 / 16, 3.2 / 16, 1.13 / 16)
465
+ displayGroup.rotation.set(0, THREE.MathUtils.degToRad(-90), THREE.MathUtils.degToRad(25), 'XYZ')
466
+ displayGroup.scale.set(0.68, 0.68, 0.68)
467
+ } else {
468
+ // Vanilla block/block firstperson_righthand defaults
469
+ displayGroup.rotation.set(0, THREE.MathUtils.degToRad(45), 0, 'XYZ')
470
+ displayGroup.scale.set(0.4, 0.4, 0.4)
471
+ }
472
+
473
+ displayGroup.add(blockInner)
474
+ blockInner = displayGroup
475
+ }
476
+
477
+ blockInner.name = 'holdingBlock'
379
478
 
380
479
  return { model: blockInner, type: handItem.type }
381
480
  }
@@ -387,6 +486,7 @@ export default class HoldingBlock {
387
486
  if (!handItem) {
388
487
  this.holdingBlock?.removeFromParent()
389
488
  this.holdingBlock = undefined
489
+ this.currentDisplayType = 'hand'
390
490
  this.swingAnimator?.stopSwing()
391
491
  this.swingAnimator = undefined
392
492
  this.idleAnimator = undefined
@@ -399,7 +499,8 @@ export default class HoldingBlock {
399
499
  // Update the model without changing the group structure
400
500
  this.holdingBlock?.removeFromParent()
401
501
  this.holdingBlock = result.model
402
- this.holdingBlockInnerGroup.add(result.model)
502
+ this.currentDisplayType = result.type
503
+ this.armTransformGroup.add(result.model)
403
504
 
404
505
 
405
506
  }
@@ -419,18 +520,21 @@ export default class HoldingBlock {
419
520
  this.lastItemModelName = undefined
420
521
  const switchRequest = ++this.switchRequest
421
522
  this.lastHeldItem = handItem
523
+
422
524
  let playAppearAnimation = false
423
525
  if (this.holdingBlock) {
424
- // play disappear animation
425
526
  playAppearAnimation = true
426
527
  const result = await this.playBlockSwapAnimation('disappeared')
427
528
  if (!result) return
428
- this.holdingBlock?.removeFromParent()
529
+ this.holdingBlock.removeFromParent()
530
+ if (this.holdingBlock !== this.playerHand) {
531
+ disposeObject(this.holdingBlock, false)
532
+ }
429
533
  this.holdingBlock = undefined
430
534
  }
431
535
 
432
536
  if (!handItem) {
433
- this.swingAnimator?.stopSwing()
537
+ this.currentDisplayType = 'hand'
434
538
  this.swingAnimator = undefined
435
539
  this.idleAnimator = undefined
436
540
  this.blockSwapAnimation = undefined
@@ -441,91 +545,20 @@ export default class HoldingBlock {
441
545
  const result = await this.createItemModel(handItem)
442
546
  if (!result || switchRequest !== this.switchRequest) return
443
547
 
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
548
  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)
549
+ this.currentDisplayType = result.type
550
+ this.armTransformGroup.add(this.holdingBlock)
464
551
 
465
552
  if (playAppearAnimation) {
466
553
  await this.playBlockSwapAnimation('appeared')
467
554
  }
468
555
 
469
- this.swingAnimator = new HandSwingAnimator(this.holdingBlockInnerGroup)
556
+ this.swingAnimator = new HandSwingAnimator()
470
557
  this.swingAnimator.type = result.type
471
- if (this.config.viewBobbing) {
472
- this.idleAnimator = new HandIdleAnimator(this.holdingBlockInnerGroup, this.worldRenderer.playerStateReactive)
473
- }
558
+ // Idle animation disabled — walking bob is handled by vanilla bobView applied to cameraGroup
559
+ this.idleAnimator = undefined
474
560
  }
475
561
 
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
562
  }
530
563
 
531
564
  class HandIdleAnimator {
@@ -750,83 +783,29 @@ class HandIdleAnimator {
750
783
  }
751
784
 
752
785
  class HandSwingAnimator {
753
- private readonly PI = Math.PI
754
786
  private animationTimer = 0
755
787
  private lastTime = 0
756
788
  private isAnimating = false
757
789
  private stopRequested = false
758
- private readonly originalRotation: THREE.Euler
759
- private readonly originalPosition: THREE.Vector3
760
- private readonly originalScale: THREE.Vector3
790
+ private swingProgress = 0
791
+ public type: 'hand' | 'block' | 'item' = 'hand'
761
792
 
762
793
  readonly debugParams = {
763
- // Animation timing
764
794
  animationTime: 250,
765
795
  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
796
  }
786
797
 
787
798
  private readonly debugGui: DebugGui
788
799
 
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
800
+ constructor() {
799
801
  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 }
802
+ animationStage: { min: 0, max: 1, step: 0.01 },
820
803
  })
821
- // this.debugGui.activate()
822
804
  }
823
805
 
824
806
  update() {
825
807
  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)
808
+ this.swingProgress = 0
830
809
  return
831
810
  }
832
811
 
@@ -834,77 +813,33 @@ class HandSwingAnimator {
834
813
  const deltaTime = (now - this.lastTime) / 1000
835
814
  this.lastTime = now
836
815
 
837
- // Update animation progress
838
- this.animationTimer += deltaTime * 1000 // Convert to ms
816
+ this.animationTimer += deltaTime * 1000
839
817
 
840
- // Calculate animation stage (0 to 1)
841
818
  const stage = this.debugParams.animationStage || Math.min(this.animationTimer / this.debugParams.animationTime, 1)
842
819
 
843
820
  if (stage >= 1) {
844
- // Animation complete
845
821
  if (this.stopRequested) {
846
- // If stop was requested, actually stop now that we've completed a swing
847
822
  this.isAnimating = false
848
823
  this.stopRequested = false
849
824
  this.animationTimer = 0
850
- this.handMesh.rotation.copy(this.originalRotation)
851
- this.handMesh.position.copy(this.originalPosition)
852
- this.handMesh.scale.copy(this.originalScale)
825
+ this.swingProgress = 0
853
826
  return
854
827
  }
855
- // Otherwise reset timer and continue
856
828
  this.animationTimer = 0
829
+ this.swingProgress = 0
857
830
  return
858
831
  }
859
832
 
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)
833
+ this.swingProgress = stage
834
+ }
896
835
 
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
- }
836
+ getSwingProgress(): number {
837
+ return this.swingProgress
902
838
  }
903
839
 
904
840
  startSwing() {
905
841
  this.stopRequested = false
906
842
  if (this.isAnimating) return
907
-
908
843
  this.isAnimating = true
909
844
  this.animationTimer = 0
910
845
  this.lastTime = performance.now()