minecraft-renderer 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -74,6 +74,7 @@
74
74
  "@types/three": "0.154.0",
75
75
  "@zardoy/react-util": "^0.2.7",
76
76
  "@zardoy/tsconfig": "^1.5.1",
77
+ "contro-max": "*",
77
78
  "esbuild": "^0.19.3",
78
79
  "esbuild-plugin-polyfill-node": "^0.3.0",
79
80
  "fs-extra": "^11.0.0",
@@ -92,11 +93,15 @@
92
93
  "vitest": "^4.0.14"
93
94
  },
94
95
  "peerDependencies": {
96
+ "contro-max": "*",
95
97
  "mc-assets": ">=0.2.0",
96
98
  "minecraft-data": ">=3.0.0",
97
99
  "three": ">=0.150.0"
98
100
  },
99
101
  "peerDependenciesMeta": {
102
+ "contro-max": {
103
+ "optional": true
104
+ },
100
105
  "mc-assets": {
101
106
  "optional": true
102
107
  },
@@ -20,7 +20,7 @@ export const defaultWorldRendererConfig = {
20
20
  // Debug settings
21
21
  showChunkBorders: false,
22
22
  enableDebugOverlay: false,
23
- debugModelVariant: undefined,
23
+ debugModelVariant: undefined as undefined | number[],
24
24
  futuristicReveal: false,
25
25
 
26
26
  // Performance settings
@@ -46,7 +46,7 @@ export const defaultWorldRendererConfig = {
46
46
  showHand: false,
47
47
  viewBobbing: false,
48
48
  renderEars: true,
49
- highlightBlockColor: 'blue' as 'blue' | 'classic' | undefined,
49
+ highlightBlockColor: 'blue' as 'blue' | 'classic' | 'auto' | undefined,
50
50
 
51
51
  // Player models
52
52
  fetchPlayerSkins: true,
@@ -59,7 +59,11 @@ export const defaultWorldRendererConfig = {
59
59
  // World settings
60
60
  clipWorldBelowY: undefined as undefined | number,
61
61
  isPlayground: false,
62
- instantCameraUpdate: false
62
+ instantCameraUpdate: false,
63
+ isRaining: false,
64
+
65
+ // Module states: 'enabled' = force on, 'disabled' = force off, 'auto' = use autoEnableCheck
66
+ moduleStates: {} as Record<string, 'enabled' | 'disabled' | 'auto'>
63
67
  }
64
68
 
65
69
  export type WorldRendererConfig = typeof defaultWorldRendererConfig
@@ -95,7 +99,7 @@ export const getDefaultRendererState = (): {
95
99
  reactive: proxy({
96
100
  world: {
97
101
  chunksLoaded: new Set<string>(),
98
- heightmaps: new Map<string, Uint8Array>(),
102
+ heightmaps: new Map<string, Int16Array>(),
99
103
  allChunksLoaded: false,
100
104
  mesherWork: false,
101
105
  intersectMedia: null
@@ -5,11 +5,11 @@
5
5
  * Core types for the graphics backend system.
6
6
  */
7
7
 
8
- import { WorldRendererConfig } from '../lib/worldrendererCommon'
9
8
  import { PlayerStateReactive } from '../playerState/playerState'
10
9
  import { ResourcesManagerTransferred } from '../resourcesManager'
11
10
  import { WorldViewWorker } from '../worldView'
12
11
  import { Vec3 } from 'vec3'
12
+ import { WorldRendererConfig } from './config'
13
13
 
14
14
  // ============================================================================
15
15
  // Graphics Backend Configuration
@@ -69,7 +69,7 @@ export interface NonReactiveState {
69
69
  export interface RendererReactiveState {
70
70
  world: {
71
71
  chunksLoaded: Set<string>
72
- heightmaps: Map<string, Uint8Array>
72
+ heightmaps: Map<string, Int16Array>
73
73
  allChunksLoaded: boolean
74
74
  mesherWork: boolean
75
75
  intersectMedia: any | null
@@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
5
5
  import TypedEmitter from 'typed-emitter'
6
6
  import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
7
7
  import { subscribeKey } from 'valtio/utils'
8
- import { proxy } from 'valtio'
8
+ import { proxy, subscribe } from 'valtio'
9
9
  import type { ResourcesManagerTransferred } from '../resourcesManager/resourcesManager'
10
10
  import { dynamicMcDataFiles } from './buildSharedConfig.mjs'
11
11
  import { DisplayWorldOptions, GraphicsInitOptions, RendererReactiveState, SoundSystem } from '../graphicsBackend/types'
@@ -64,7 +64,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
64
64
  dirty(pos: Vec3, value: boolean): void
65
65
  update(/* pos: Vec3, value: boolean */): void
66
66
  chunkFinished(key: string): void
67
- heightmap(key: string, heightmap: Uint8Array): void
67
+ heightmap(key: string, heightmap: Int16Array): void
68
68
  }>
69
69
  customTexturesDataUrl = undefined as string | undefined
70
70
  workers: any[] = []
@@ -106,6 +106,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
106
106
  debugStopGeometryUpdate = false
107
107
 
108
108
  protocolCustomBlocks = new Map<string, CustomBlockModels>()
109
+ private heightmapDebounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
109
110
 
110
111
  blockStateModelInfo = new Map<string, BlockStateModelInfo>()
111
112
 
@@ -115,6 +116,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
115
116
 
116
117
  abstract changeBackgroundColor(color: [number, number, number]): void
117
118
 
119
+ /** Override in subclass to check if any enabled module requires heightmap data */
120
+ protected anyModuleRequiresHeightmap(): boolean {
121
+ return false
122
+ }
123
+
118
124
  worldRendererConfig: WorldRendererConfig
119
125
  playerStateReactive: PlayerStateReactive
120
126
  playerStateUtils: PlayerStateUtils
@@ -279,7 +285,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
279
285
 
280
286
  onReactiveConfigUpdated<T extends keyof typeof this.worldRendererConfig>(key: T, callback: (value: typeof this.worldRendererConfig[T]) => void) {
281
287
  callback(this.worldRendererConfig[key])
282
- subscribeKey(this.worldRendererConfig, key, callback)
288
+ if ((key as any) === '*') {
289
+ subscribe(this.worldRendererConfig, callback as any)
290
+ } else {
291
+ subscribeKey(this.worldRendererConfig, key, callback)
292
+ }
283
293
  }
284
294
 
285
295
  onReactiveDebugUpdated<T extends keyof typeof this.reactiveDebugParams>(key: T, callback: (value: typeof this.reactiveDebugParams[T]) => void) {
@@ -428,7 +438,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
428
438
  }
429
439
 
430
440
  if (data.type === 'heightmap') {
431
- this.reactiveState.world.heightmaps.set(data.key, new Uint8Array(data.heightmap))
441
+ this.reactiveState.world.heightmaps.set(data.key, new Int16Array(data.heightmap))
432
442
  }
433
443
  }
434
444
 
@@ -622,11 +632,11 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
622
632
  customBlockModels: customBlockModels || undefined
623
633
  })
624
634
  }
625
- // this.workers[0].postMessage({
626
- // type: 'getHeightmap',
627
- // x,
628
- // z,
629
- // })
635
+ this.workers[0].postMessage({
636
+ type: 'getHeightmap',
637
+ x,
638
+ z,
639
+ })
630
640
  this.logWorkerWork(() => `-> chunk ${JSON.stringify({ x, z, chunkLength: chunk.length, customBlockModelsLength: customBlockModels ? Object.keys(customBlockModels).length : 0 })}`)
631
641
  this.mesherLogReader?.chunkReceived(x, z, chunk.length)
632
642
  const sectionHeight = this.getSectionHeight()
@@ -665,6 +675,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
665
675
 
666
676
  removeColumn(x, z) {
667
677
  delete this.loadedChunks[`${x},${z}`]
678
+ // Cancel any pending heightmap debounce for this chunk
679
+ const debounceKey = `${x},${z}`
680
+ const pendingTimer = this.heightmapDebounceTimers.get(debounceKey)
681
+ if (pendingTimer) {
682
+ clearTimeout(pendingTimer)
683
+ this.heightmapDebounceTimers.delete(debounceKey)
684
+ }
668
685
  for (const worker of this.workers) {
669
686
  worker.postMessage({ type: 'unloadChunk', x, z })
670
687
  }
@@ -687,6 +704,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
687
704
  }
688
705
  }
689
706
  this.highestBlocksByChunks.delete(`${x},${z}`)
707
+ this.reactiveState.world.heightmaps.delete(`${Math.floor(x / 16)},${Math.floor(z / 16)}`)
690
708
 
691
709
  this.updateChunksStats()
692
710
 
@@ -848,6 +866,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
848
866
  customBlockModels
849
867
  })
850
868
  }
869
+ // Re-request heightmap for the affected chunk after block change (debounced)
870
+ if (this.anyModuleRequiresHeightmap()) {
871
+ const chunkCornerX = Math.floor(pos.x / CHUNK_SIZE) * CHUNK_SIZE
872
+ const chunkCornerZ = Math.floor(pos.z / CHUNK_SIZE) * CHUNK_SIZE
873
+ const chunkKey2 = `${chunkCornerX},${chunkCornerZ}`
874
+ const existing = this.heightmapDebounceTimers.get(chunkKey2)
875
+ if (existing) clearTimeout(existing)
876
+ this.heightmapDebounceTimers.set(chunkKey2, setTimeout(() => {
877
+ this.heightmapDebounceTimers.delete(chunkKey2)
878
+ this.workers[0]?.postMessage({ type: 'getHeightmap', x: chunkCornerX, z: chunkCornerZ })
879
+ }, 100))
880
+ }
851
881
  this.logWorkerWork(`-> blockUpdate ${JSON.stringify({ pos, stateId, customBlockModels })}`)
852
882
  this.setSectionDirty(pos, true, true)
853
883
  if (this.neighborChunkUpdates) {
@@ -1018,6 +1048,12 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
1018
1048
  }
1019
1049
 
1020
1050
  destroy() {
1051
+ // Cancel all pending heightmap debounce timers
1052
+ for (const timer of this.heightmapDebounceTimers.values()) {
1053
+ clearTimeout(timer)
1054
+ }
1055
+ this.heightmapDebounceTimers.clear()
1056
+
1021
1057
  // Stop all workers
1022
1058
  for (const worker of this.workers) {
1023
1059
  worker.terminate()
@@ -153,7 +153,7 @@ const handleMessage = data => {
153
153
  break
154
154
  }
155
155
  case 'getHeightmap': {
156
- const heightmap = new Uint8Array(256)
156
+ const heightmap = new Int16Array(256)
157
157
 
158
158
  const blockPos = new Vec3(0, 0, 0)
159
159
  for (let z = 0; z < 16; z++) {
@@ -169,7 +169,7 @@ const handleMessage = data => {
169
169
  block = world.getBlock(blockPos)
170
170
  }
171
171
  const index = z * 16 + x
172
- heightmap[index] = block ? blockPos.y : 0
172
+ heightmap[index] = block ? blockPos.y : -32768
173
173
  }
174
174
  }
175
175
  postMessage({ type: 'heightmap', key: `${Math.floor(data.x / 16)},${Math.floor(data.z / 16)}`, heightmap })
@@ -65,7 +65,7 @@ export interface MesherMainEvents {
65
65
  geometry: { type: 'geometry'; key: string; geometry: MesherGeometryOutput; workerIndex: number };
66
66
  sectionFinished: { type: 'sectionFinished'; key: string; workerIndex: number; processTime?: number };
67
67
  blockStateModelInfo: { type: 'blockStateModelInfo'; info: Record<string, BlockStateModelInfo> };
68
- heightmap: { type: 'heightmap'; key: string; heightmap: Uint8Array };
68
+ heightmap: { type: 'heightmap'; key: string; heightmap: Int16Array };
69
69
  }
70
70
 
71
71
  export type MesherMainEvent = MesherMainEvents[keyof MesherMainEvents]
@@ -1,7 +1,10 @@
1
1
  //@ts-nocheck
2
+ import { IndexedData } from 'minecraft-data'
2
3
  import { EntityMesh, rendererSpecialHandled, EntityDebugFlags } from '../three/entity/EntityMesh'
3
4
 
4
- export const displayEntitiesDebugList = (version: string) => {
5
+ export const displayEntitiesDebugList = (mcData: IndexedData) => {
6
+ const version = mcData.version.minecraftVersion!
7
+
5
8
  // Create results container
6
9
  const container = document.createElement('div')
7
10
  container.style.cssText = `
@@ -36,7 +39,6 @@ export const displayEntitiesDebugList = (version: string) => {
36
39
  textureMap?: boolean;
37
40
  errors?: string[];
38
41
  }> = []
39
- const { mcData } = window
40
42
  const entityNames = Object.keys(mcData.entitiesArray.reduce((acc, entity) => {
41
43
  acc[entity.name] = true
42
44
  return acc
@@ -64,8 +66,8 @@ export const displayEntitiesDebugList = (version: string) => {
64
66
 
65
67
  const { mesh: entityMesh } = new EntityMesh(version, entity, undefined, {}, debugFlags)
66
68
  // find the most distant pos child
67
- window.objects ??= {}
68
- window.objects[entity] = entityMesh
69
+ globalThis.objects ??= {}
70
+ globalThis.objects[entity] = entityMesh
69
71
 
70
72
  results.push({
71
73
  entity,
@@ -8,6 +8,6 @@ export default class AllEntities extends BasePlaygroundScene {
8
8
 
9
9
  async initData() {
10
10
  await super.initData()
11
- displayEntitiesDebugList(this.version)
11
+ displayEntitiesDebugList(this.mcData)
12
12
  }
13
13
  }
@@ -27,7 +27,7 @@ export default class RailsCobwebScene extends BasePlaygroundScene {
27
27
  for (let x = -squareSize; x <= squareSize; x++) {
28
28
  for (let z = -squareSize; z <= squareSize; z++) {
29
29
  const i = Math.abs(x + z) * squareSize
30
- this.worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(fullBlocks[i % fullBlocks.length].defaultState, 0))
30
+ this.worldView!.setBlockStateId(this.targetPos.offset(x, 0, z), this.mcData.blocksByName[fullBlocks[i % fullBlocks.length].name]!.defaultState)
31
31
  }
32
32
  }
33
33
  }
@@ -25,8 +25,8 @@ export const getItemUv = (item: Record<string, any>, specificProps: ItemSpecific
25
25
  try {
26
26
  const name =
27
27
  blockState
28
- ? loadedData.blocksByStateId[blockState]?.name
29
- : typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
28
+ ? globalThis.loadedData.blocksByStateId[blockState]?.name
29
+ : typeof idOrName === 'number' ? globalThis.loadedData.items[idOrName]?.name : idOrName
30
30
  if (!name) throw new Error(`Item not found: ${idOrName}`)
31
31
 
32
32
  const model = getItemModelName({
@@ -13,8 +13,8 @@ import * as THREE from 'three'
13
13
  import Stats from 'stats.js'
14
14
  import StatsGl from 'stats-gl'
15
15
  import * as tween from '@tweenjs/tween.js'
16
- import { WorldRendererConfig } from '../lib/worldrendererCommon'
17
16
  import type { GraphicsInitOptions } from '../graphicsBackend/types'
17
+ import { WorldRendererConfig } from '../graphicsBackend'
18
18
 
19
19
  // ============================================================================
20
20
  // Types (co-located with implementation)
@@ -208,7 +208,7 @@ export class DocumentRenderer {
208
208
 
209
209
  try {
210
210
  this.renderer = new THREE.WebGLRenderer({
211
- canvas: this.canvas,
211
+ canvas: this.canvas as HTMLCanvasElement,
212
212
  preserveDrawingBuffer: true,
213
213
  logarithmicDepthBuffer: true,
214
214
  powerPreference: this.config.powerPreference
@@ -66,6 +66,8 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
66
66
  launchFirework: worldRenderer.fireworks.launchFirework.bind(worldRenderer.fireworks),
67
67
  // New method for updating skybox
68
68
  setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer),
69
+ // Rain methods
70
+ setRain: (newState: boolean) => worldRenderer.toggleModule('rain', newState),
69
71
  async loadGeometryExport(exportData: any) {
70
72
  // Import dynamically to avoid circular dependencies
71
73
  const { applyWorldGeometryExport } = await import('./worldGeometryExport')
@@ -7,7 +7,6 @@ import { BlockModel } from 'mc-assets'
7
7
  import { DebugGui } from '../lib/DebugGui'
8
8
  import { SmoothSwitcher } from '../lib/smoothSwitcher'
9
9
  import { watchProperty } from '../lib/utils/proxy'
10
- import { WorldRendererConfig } from '../lib/worldrendererCommon'
11
10
  import { getMyHand } from './hand'
12
11
  import { WorldRendererThree } from './worldRendererThree'
13
12
  import { disposeObject } from './threeJsUtils'
@@ -16,6 +15,7 @@ import { PlayerStateRenderer } from '../playerState/playerState'
16
15
  import { getThreeBlockModelGroup } from '../mesher/standaloneRenderer'
17
16
  import { IndexedData } from 'minecraft-data'
18
17
  import type { ResourcesManagerTransferred } from '../resourcesManager'
18
+ import { WorldRendererConfig } from '../graphicsBackend'
19
19
 
20
20
  const rotationPositionData = {
21
21
  itemRight: {
@@ -1,8 +1,10 @@
1
1
  //@ts-nocheck
2
+ import { rainManifest } from './rain'
2
3
  import { sciFiWorldRevealManifest } from './sciFiWorldReveal'
3
4
  import { starfieldManifest } from './starfield'
4
5
 
5
6
  export const BUILTIN_MODULES = {
6
7
  starfield: starfieldManifest,
7
8
  futuristicReveal: sciFiWorldRevealManifest,
9
+ rain: rainManifest,
8
10
  }
@@ -0,0 +1,185 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import type { WorldRendererThree } from '../worldRendererThree'
4
+ import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
5
+
6
+ interface RainParticleData {
7
+ velocity: THREE.Vector3
8
+ age: number
9
+ despawnOffset: number
10
+ }
11
+
12
+ const PARTICLE_COUNT = 2000
13
+ const RANGE = 32
14
+ const HEIGHT = 32
15
+ const FALL_SPEED_MIN = 0.2
16
+ const FALL_SPEED_MAX = 0.4
17
+ const HORIZONTAL_DRIFT = 0.02
18
+ const RESPAWN_BELOW = -5
19
+
20
+ export class RainModule implements RendererModuleController {
21
+ private instancedMesh?: THREE.InstancedMesh
22
+ private geometry?: THREE.BoxGeometry
23
+ private material?: THREE.MeshBasicMaterial
24
+ private particles: RainParticleData[] = []
25
+ private enabled = false
26
+ private readonly dummy = new THREE.Matrix4()
27
+ private readonly tempPosition = new THREE.Vector3()
28
+ private readonly tempQuaternion = new THREE.Quaternion()
29
+ private readonly tempScale = new THREE.Vector3()
30
+
31
+ constructor(private readonly worldRenderer: WorldRendererThree) { }
32
+
33
+ enable(): void {
34
+ if (this.enabled) return
35
+ this.enabled = true
36
+ if (!this.instancedMesh) {
37
+ this.createRain()
38
+ } else {
39
+ this.instancedMesh.visible = true
40
+ }
41
+ }
42
+
43
+ disable(): void {
44
+ if (!this.enabled) return
45
+ this.enabled = false
46
+ if (this.instancedMesh) {
47
+ this.instancedMesh.visible = false
48
+ }
49
+ }
50
+
51
+ autoEnableCheck(): boolean {
52
+ return this.worldRenderer.worldRendererConfig.isRaining === true
53
+ }
54
+
55
+ render?: () => void = () => {
56
+ if (!this.enabled || !this.instancedMesh) return
57
+
58
+ const cameraPos = this.worldRenderer.getCameraPosition()
59
+ this.instancedMesh.position.copy(cameraPos)
60
+
61
+ const heightmaps = this.worldRenderer.reactiveState.world.heightmaps
62
+
63
+ const { dummy, tempPosition: position, tempQuaternion: quaternion, tempScale: scale } = this
64
+
65
+ // Cache chunk key lookup to avoid redundant Map.get and string allocation
66
+ let prevChunkX = NaN
67
+ let prevChunkZ = NaN
68
+ let cachedHeightmap: Int16Array | undefined
69
+
70
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
71
+ const particle = this.particles[i]
72
+ this.instancedMesh.getMatrixAt(i, dummy)
73
+ dummy.decompose(position, quaternion, scale)
74
+
75
+ position.add(particle.velocity)
76
+
77
+ const relativeY = position.y
78
+ const horizontalDist = Math.sqrt(position.x * position.x + position.z * position.z)
79
+
80
+ // Convert camera-relative position to world coordinates
81
+ const worldX = cameraPos.x + position.x
82
+ const worldY = cameraPos.y + position.y
83
+ const worldZ = cameraPos.z + position.z
84
+
85
+ // Look up heightmap for this world position (cached per chunk)
86
+ const chunkX = Math.floor(worldX / 16)
87
+ const chunkZ = Math.floor(worldZ / 16)
88
+ if (chunkX !== prevChunkX || chunkZ !== prevChunkZ) {
89
+ cachedHeightmap = heightmaps.get(`${chunkX},${chunkZ}`)
90
+ prevChunkX = chunkX
91
+ prevChunkZ = chunkZ
92
+ }
93
+
94
+ const localX = ((Math.floor(worldX) % 16) + 16) % 16
95
+ const localZ = ((Math.floor(worldZ) % 16) + 16) % 16
96
+ const heightY = cachedHeightmap?.[localZ * 16 + localX]
97
+
98
+ // Respawn when: out of range, hit heightmap surface (heightY + 1 = block top face), or fell too far
99
+ const shouldRespawn = horizontalDist > RANGE ||
100
+ (heightY !== undefined && heightY !== -32768 && worldY <= heightY + 1 + particle.despawnOffset) ||
101
+ relativeY < RESPAWN_BELOW
102
+
103
+ if (shouldRespawn) {
104
+ this.respawnParticle(position)
105
+ const speed = FALL_SPEED_MIN + Math.random() * (FALL_SPEED_MAX - FALL_SPEED_MIN)
106
+ particle.velocity.set(
107
+ (Math.random() - 0.5) * HORIZONTAL_DRIFT,
108
+ -speed,
109
+ (Math.random() - 0.5) * HORIZONTAL_DRIFT,
110
+ )
111
+ particle.despawnOffset = Math.random() * 0.5
112
+ }
113
+
114
+ dummy.compose(position, quaternion, scale)
115
+ this.instancedMesh.setMatrixAt(i, dummy)
116
+ }
117
+
118
+ this.instancedMesh.instanceMatrix.needsUpdate = true
119
+ }
120
+
121
+ dispose(): void {
122
+ if (this.instancedMesh) {
123
+ this.worldRenderer.scene.remove(this.instancedMesh)
124
+ }
125
+ this.geometry?.dispose()
126
+ this.material?.dispose()
127
+ this.instancedMesh = undefined
128
+ this.geometry = undefined
129
+ this.material = undefined
130
+ this.particles = []
131
+ }
132
+
133
+ private createRain(): void {
134
+ this.geometry = new THREE.BoxGeometry(0.03, 0.3, 0.03)
135
+ this.material = new THREE.MeshBasicMaterial({
136
+ color: 0x44_66_99,
137
+ transparent: true,
138
+ opacity: 0.6,
139
+ })
140
+
141
+ this.instancedMesh = new THREE.InstancedMesh(this.geometry, this.material, PARTICLE_COUNT)
142
+ this.instancedMesh.name = 'rain-particles'
143
+
144
+ const dummy = new THREE.Matrix4()
145
+ const position = new THREE.Vector3()
146
+
147
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
148
+ this.respawnParticle(position)
149
+ position.y = Math.random() * HEIGHT
150
+ dummy.setPosition(position)
151
+ this.instancedMesh.setMatrixAt(i, dummy)
152
+
153
+ const speed = FALL_SPEED_MIN + Math.random() * (FALL_SPEED_MAX - FALL_SPEED_MIN)
154
+ this.particles.push({
155
+ velocity: new THREE.Vector3(
156
+ (Math.random() - 0.5) * HORIZONTAL_DRIFT,
157
+ -speed,
158
+ (Math.random() - 0.5) * HORIZONTAL_DRIFT,
159
+ ),
160
+ age: 0,
161
+ despawnOffset: Math.random() * 0.5,
162
+ })
163
+ }
164
+
165
+ this.instancedMesh.instanceMatrix.needsUpdate = true
166
+ this.worldRenderer.scene.add(this.instancedMesh)
167
+ }
168
+
169
+ private respawnParticle(position: THREE.Vector3): void {
170
+ const angle = Math.random() * Math.PI * 2
171
+ const distance = Math.random() * RANGE
172
+ position.set(
173
+ Math.cos(angle) * distance,
174
+ HEIGHT,
175
+ Math.sin(angle) * distance,
176
+ )
177
+ }
178
+ }
179
+
180
+ export const rainManifest: RendererModuleManifest = {
181
+ id: 'rain',
182
+ controller: RainModule,
183
+ enabledDefault: false,
184
+ requiresHeightmap: true,
185
+ }
@@ -99,6 +99,15 @@ export class SciFiWorldRevealModule implements RendererModuleController {
99
99
  this.reset()
100
100
  }
101
101
 
102
+ toggle(): boolean {
103
+ if (this.enabled) {
104
+ this.disable()
105
+ } else {
106
+ this.enable()
107
+ }
108
+ return this.enabled
109
+ }
110
+
102
111
  render?: () => void = () => {
103
112
  if (!this.enabled) return
104
113
  this.update(16)
@@ -56,6 +56,15 @@ export class StarfieldModule implements RendererModuleController {
56
56
  this.removeStars()
57
57
  }
58
58
 
59
+ toggle(): boolean {
60
+ if (this.enabled) {
61
+ this.disable()
62
+ } else {
63
+ this.enable()
64
+ }
65
+ return this.enabled
66
+ }
67
+
59
68
  enablementCheck?: () => boolean = () => {
60
69
  if (!this.currentTime) return false
61
70
  const nightTime = 13_500
@@ -17,10 +17,10 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res
17
17
  modelName: string | null,
18
18
  } => {
19
19
  let itemModelName = model.modelName
20
- const isItem = loadedData.itemsByName[itemModelName]
20
+ const isItem = globalThis.loadedData.itemsByName[itemModelName]
21
21
 
22
22
  // #region normalize item name
23
- if (versionToNumber(bot.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, bot.version, '1.13.1') as string
23
+ if (versionToNumber(resourcesManager.currentResources!.version) < versionToNumber('1.13')) itemModelName = getRenamedData(isItem ? 'items' : 'blocks', itemModelName, resourcesManager.currentResources!.version, '1.13.1') as string
24
24
  // #endregion
25
25
 
26
26
 
@@ -47,18 +47,18 @@ export const renderSlot = (model: ResolvedItemModelRender, resourcesManager: Res
47
47
  const blockToTopTexture = (r) => r.top ?? r
48
48
 
49
49
  try {
50
- if (!appViewer.resourcesManager.currentResources?.itemsRenderer) throw new Error('Items renderer is not available')
50
+ if (!resourcesManager.currentResources?.itemsRenderer) throw new Error('Items renderer is not available')
51
51
  itemTexture =
52
- appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport)
53
- ?? (model.originalItemName ? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined)
54
- ?? appViewer.resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')!
52
+ resourcesManager.currentResources.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport)
53
+ ?? (model.originalItemName ? resourcesManager.currentResources.itemsRenderer.getItemTexture(model.originalItemName, {}, false, fullBlockModelSupport) : undefined)
54
+ ?? resourcesManager.currentResources.itemsRenderer.getItemTexture('item/missing_texture')!
55
55
  } catch (err) {
56
56
  // get resourcepack from resource manager
57
- reportError?.(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${bot.version} (resourcepack: TODO!): ${err.stack}`)
58
- itemTexture = blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer!.getItemTexture('errored')!)
57
+ reportError?.(`Failed to render item ${itemModelName} (original: ${model.originalItemName}) on ${resourcesManager.currentResources!.version} (resourcepack: TODO!): ${err.stack}`)
58
+ itemTexture = blockToTopTexture(resourcesManager.currentResources!.itemsRenderer!.getItemTexture('errored')!)
59
59
  }
60
60
 
61
- itemTexture ??= blockToTopTexture(appViewer.resourcesManager.currentResources!.itemsRenderer!.getItemTexture('unknown')!)
61
+ itemTexture ??= blockToTopTexture(resourcesManager.currentResources!.itemsRenderer!.getItemTexture('unknown')!)
62
62
 
63
63
 
64
64
  if ('type' in itemTexture) {
@@ -10,6 +10,7 @@ export interface RendererModuleController {
10
10
  dispose(): void
11
11
 
12
12
  enablementCheck?: () => boolean
13
+ autoEnableCheck?: () => boolean // Called when config updates, returns true to enable, false to disable
13
14
  render?: () => void
14
15
  }
15
16
 
@@ -29,10 +30,13 @@ export interface RendererModuleManifest {
29
30
  cannotBeDisabled?: boolean
30
31
  slowSystemAutoDisable?: boolean
31
32
  userSettingsSchema?: Record<string, any>
33
+
34
+ requiresHeightmap?: boolean
32
35
  }
33
36
 
34
37
  export interface RegisteredModule {
35
38
  manifest: RendererModuleManifest
36
39
  controller: RendererModuleController
37
40
  enabled: boolean
41
+ toggle: () => boolean
38
42
  }