- Introduced a patchAssets script to apply custom textures to the blocks and items atlases.
- Enhanced the ThreeJsSound class to support sound playback timeout and volume adjustments. - Added a custom sound system to handle named sound effects with metadata.
This commit is contained in:
parent
cc4f705aea
commit
e1293b6cb3
11 changed files with 323 additions and 18 deletions
2
assets/customTextures/readme.md
Normal file
2
assets/customTextures/readme.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png
|
||||
get file names from here (blocks/items) https://zardoy.github.io/mc-assets/
|
||||
|
|
@ -2,7 +2,7 @@ import * as THREE from 'three'
|
|||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
||||
export interface SoundSystem {
|
||||
playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number) => void
|
||||
playSound: (position: { x: number, y: number, z: number }, path: string, volume?: number, pitch?: number, timeout?: number) => void
|
||||
destroy: () => void
|
||||
}
|
||||
|
||||
|
|
@ -10,7 +10,17 @@ export class ThreeJsSound implements SoundSystem {
|
|||
audioListener: THREE.AudioListener | undefined
|
||||
private readonly activeSounds = new Set<THREE.PositionalAudio>()
|
||||
private readonly audioContext: AudioContext | undefined
|
||||
private readonly soundVolumes = new Map<THREE.PositionalAudio, number>()
|
||||
baseVolume = 1
|
||||
|
||||
constructor (public worldRenderer: WorldRendererThree) {
|
||||
worldRenderer.onWorldSwitched.push(() => {
|
||||
this.stopAll()
|
||||
})
|
||||
|
||||
worldRenderer.onReactiveConfigUpdated('volume', (volume) => {
|
||||
this.changeVolume(volume)
|
||||
})
|
||||
}
|
||||
|
||||
initAudioListener () {
|
||||
|
|
@ -19,20 +29,24 @@ export class ThreeJsSound implements SoundSystem {
|
|||
this.worldRenderer.camera.add(this.audioListener)
|
||||
}
|
||||
|
||||
playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1) {
|
||||
playSound (position: { x: number, y: number, z: number }, path: string, volume = 1, pitch = 1, timeout = 500) {
|
||||
this.initAudioListener()
|
||||
|
||||
const sound = new THREE.PositionalAudio(this.audioListener!)
|
||||
this.activeSounds.add(sound)
|
||||
this.soundVolumes.set(sound, volume)
|
||||
|
||||
const audioLoader = new THREE.AudioLoader()
|
||||
const start = Date.now()
|
||||
void audioLoader.loadAsync(path).then((buffer) => {
|
||||
if (Date.now() - start > 500) return
|
||||
if (Date.now() - start > timeout) {
|
||||
console.warn('Ignored playing sound', path, 'due to timeout:', timeout, 'ms <', Date.now() - start, 'ms')
|
||||
return
|
||||
}
|
||||
// play
|
||||
sound.setBuffer(buffer)
|
||||
sound.setRefDistance(20)
|
||||
sound.setVolume(volume)
|
||||
sound.setVolume(volume * this.baseVolume)
|
||||
sound.setPlaybackRate(pitch) // set the pitch
|
||||
this.worldRenderer.scene.add(sound)
|
||||
// set sound position
|
||||
|
|
@ -43,21 +57,35 @@ export class ThreeJsSound implements SoundSystem {
|
|||
sound.disconnect()
|
||||
}
|
||||
this.activeSounds.delete(sound)
|
||||
this.soundVolumes.delete(sound)
|
||||
audioLoader.manager.itemEnd(path)
|
||||
}
|
||||
sound.play()
|
||||
})
|
||||
}
|
||||
|
||||
destroy () {
|
||||
// Stop and clean up all active sounds
|
||||
stopAll () {
|
||||
for (const sound of this.activeSounds) {
|
||||
if (!sound) continue
|
||||
sound.stop()
|
||||
if (sound.source) {
|
||||
sound.disconnect()
|
||||
}
|
||||
this.worldRenderer.scene.remove(sound)
|
||||
}
|
||||
this.activeSounds.clear()
|
||||
this.soundVolumes.clear()
|
||||
}
|
||||
|
||||
changeVolume (volume: number) {
|
||||
this.baseVolume = volume
|
||||
for (const [sound, individualVolume] of this.soundVolumes) {
|
||||
sound.setVolume(individualVolume * this.baseVolume)
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.stopAll()
|
||||
// Remove and cleanup audio listener
|
||||
if (this.audioListener) {
|
||||
this.audioListener.removeFromParent()
|
||||
|
|
|
|||
|
|
@ -240,6 +240,10 @@ const appConfig = defineConfig({
|
|||
prep()
|
||||
})
|
||||
build.onAfterBuild(async () => {
|
||||
if (fs.readdirSync('./assets/customTextures').length > 0) {
|
||||
childProcess.execSync('tsx ./scripts/patchAssets.ts', { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
if (SINGLE_FILE_BUILD) {
|
||||
// check that only index.html is in the dist/single folder
|
||||
const singleBuildFiles = fs.readdirSync('./dist/single')
|
||||
|
|
|
|||
137
scripts/patchAssets.ts
Normal file
137
scripts/patchAssets.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import blocksAtlas from 'mc-assets/dist/blocksAtlases.json'
|
||||
import itemsAtlas from 'mc-assets/dist/itemsAtlases.json'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import sharp from 'sharp'
|
||||
|
||||
interface AtlasFile {
|
||||
latest: {
|
||||
suSv: number
|
||||
tileSize: number
|
||||
width: number
|
||||
height: number
|
||||
textures: {
|
||||
[key: string]: {
|
||||
u: number
|
||||
v: number
|
||||
su: number
|
||||
sv: number
|
||||
tileIndex: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function patchTextureAtlas(
|
||||
atlasType: 'blocks' | 'items',
|
||||
atlasData: AtlasFile,
|
||||
customTexturesDir: string,
|
||||
distDir: string
|
||||
) {
|
||||
// Check if custom textures directory exists and has files
|
||||
if (!fs.existsSync(customTexturesDir) || fs.readdirSync(customTexturesDir).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the latest atlas file
|
||||
const atlasFiles = fs.readdirSync(distDir)
|
||||
.filter(file => file.startsWith(`${atlasType}AtlasLatest`) && file.endsWith('.png'))
|
||||
.sort()
|
||||
|
||||
if (atlasFiles.length === 0) {
|
||||
console.log(`No ${atlasType}AtlasLatest.png found in ${distDir}`)
|
||||
return
|
||||
}
|
||||
|
||||
const latestAtlasFile = atlasFiles[atlasFiles.length - 1]
|
||||
const atlasPath = path.join(distDir, latestAtlasFile)
|
||||
console.log(`Patching ${atlasPath}`)
|
||||
|
||||
// Get atlas dimensions
|
||||
const atlasMetadata = await sharp(atlasPath).metadata()
|
||||
if (!atlasMetadata.width || !atlasMetadata.height) {
|
||||
throw new Error(`Failed to get atlas dimensions for ${atlasPath}`)
|
||||
}
|
||||
|
||||
// Process each custom texture
|
||||
const customTextureFiles = fs.readdirSync(customTexturesDir)
|
||||
.filter(file => file.endsWith('.png'))
|
||||
|
||||
if (customTextureFiles.length === 0) return
|
||||
|
||||
// Prepare composite operations
|
||||
const composites: sharp.OverlayOptions[] = []
|
||||
|
||||
for (const textureFile of customTextureFiles) {
|
||||
const textureName = path.basename(textureFile, '.png')
|
||||
|
||||
if (atlasData.latest.textures[textureName]) {
|
||||
const textureData = atlasData.latest.textures[textureName]
|
||||
const customTexturePath = path.join(customTexturesDir, textureFile)
|
||||
|
||||
try {
|
||||
// Convert UV coordinates to pixel coordinates
|
||||
const x = Math.round(textureData.u * atlasMetadata.width)
|
||||
const y = Math.round(textureData.v * atlasMetadata.height)
|
||||
const width = Math.round((textureData.su ?? atlasData.latest.suSv) * atlasMetadata.width)
|
||||
const height = Math.round((textureData.sv ?? atlasData.latest.suSv) * atlasMetadata.height)
|
||||
|
||||
// Resize custom texture to match atlas dimensions and add to composite operations
|
||||
const resizedTextureBuffer = await sharp(customTexturePath)
|
||||
.resize(width, height, {
|
||||
fit: 'fill',
|
||||
kernel: 'nearest' // Preserve pixel art quality
|
||||
})
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
composites.push({
|
||||
input: resizedTextureBuffer,
|
||||
left: x,
|
||||
top: y,
|
||||
blend: 'over'
|
||||
})
|
||||
|
||||
console.log(`Prepared ${textureName} at (${x}, ${y}) with size (${width}, ${height})`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to prepare ${textureName}:`, error)
|
||||
}
|
||||
} else {
|
||||
console.warn(`Texture ${textureName} not found in ${atlasType} atlas`)
|
||||
}
|
||||
}
|
||||
|
||||
if (composites.length > 0) {
|
||||
// Apply all patches at once using Sharp's composite
|
||||
await sharp(atlasPath)
|
||||
.composite(composites)
|
||||
.png()
|
||||
.toFile(atlasPath + '.tmp')
|
||||
|
||||
// Replace original with patched version
|
||||
fs.renameSync(atlasPath + '.tmp', atlasPath)
|
||||
console.log(`Saved patched ${atlasType} atlas to ${atlasPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const customBlocksDir = './assets/customTextures/blocks'
|
||||
const customItemsDir = './assets/customTextures/items'
|
||||
const distDir = './dist/static/image'
|
||||
|
||||
try {
|
||||
// Patch blocks atlas
|
||||
await patchTextureAtlas('blocks', blocksAtlas as unknown as AtlasFile, customBlocksDir, distDir)
|
||||
|
||||
// Patch items atlas
|
||||
await patchTextureAtlas('items', itemsAtlas as unknown as AtlasFile, customItemsDir, distDir)
|
||||
|
||||
console.log('Texture atlas patching completed!')
|
||||
} catch (error) {
|
||||
console.error('Failed to patch texture atlases:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
main()
|
||||
|
|
@ -43,7 +43,7 @@ export async function loadSound (path: string, contents = path) {
|
|||
}
|
||||
}
|
||||
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => {
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => {
|
||||
const soundBuffer = sounds[url]
|
||||
if (!soundBuffer) {
|
||||
const start = Date.now()
|
||||
|
|
@ -51,10 +51,10 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) =
|
|||
if (cancelled || Date.now() - start > loadTimeout) return
|
||||
}
|
||||
|
||||
return playSound(url, soundVolume)
|
||||
return playSound(url, soundVolume, loop)
|
||||
}
|
||||
|
||||
export async function playSound (url, soundVolume = 1) {
|
||||
export async function playSound (url, soundVolume = 1, loop = false) {
|
||||
const volume = soundVolume * (options.volume / 100)
|
||||
|
||||
if (!volume) return
|
||||
|
|
@ -75,6 +75,7 @@ export async function playSound (url, soundVolume = 1) {
|
|||
const gainNode = audioContext.createGain()
|
||||
const source = audioContext.createBufferSource()
|
||||
source.buffer = soundBuffer
|
||||
source.loop = loop
|
||||
source.connect(gainNode)
|
||||
gainNode.connect(audioContext.destination)
|
||||
gainNode.gain.value = volume
|
||||
|
|
@ -99,6 +100,16 @@ export async function playSound (url, soundVolume = 1) {
|
|||
onEnded (callback: () => void) {
|
||||
callbacks.push(callback)
|
||||
},
|
||||
stop () {
|
||||
try {
|
||||
source.stop()
|
||||
// Remove from active sounds
|
||||
const index = activeSounds.findIndex(s => s.source === source)
|
||||
if (index !== -1) activeSounds.splice(index, 1)
|
||||
} catch (err) {
|
||||
console.warn('Failed to stop sound:', err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +124,19 @@ export function stopAllSounds () {
|
|||
activeSounds.length = 0
|
||||
}
|
||||
|
||||
export function stopSound (url: string) {
|
||||
const soundIndex = activeSounds.findIndex(s => s.source.buffer === sounds[url])
|
||||
if (soundIndex !== -1) {
|
||||
const { source } = activeSounds[soundIndex]
|
||||
try {
|
||||
source.stop()
|
||||
} catch (err) {
|
||||
console.warn('Failed to stop sound:', err)
|
||||
}
|
||||
activeSounds.splice(soundIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||
const normalizedVolume = newVolume / 100
|
||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export const defaultOptions = {
|
|||
} as any,
|
||||
preferLoadReadonly: false,
|
||||
experimentalClientSelfReload: true,
|
||||
remoteSoundsLoadTimeout: 500,
|
||||
disableLoadPrompts: false,
|
||||
guestUsername: 'guest',
|
||||
askGuestName: true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Slider.tsx
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import styles from './slider.module.css'
|
||||
import SharedHudVars from './SharedHudVars'
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ interface Props extends React.ComponentProps<'div'> {
|
|||
min?: number;
|
||||
max?: number;
|
||||
disabledReason?: string;
|
||||
throttle?: number | false; // milliseconds, default 100, false to disable
|
||||
|
||||
updateValue?: (value: number) => void;
|
||||
updateOnDragEnd?: boolean;
|
||||
|
|
@ -26,15 +27,24 @@ const Slider: React.FC<Props> = ({
|
|||
min = 0,
|
||||
max = 100,
|
||||
disabledReason,
|
||||
throttle = 0,
|
||||
|
||||
updateOnDragEnd = false,
|
||||
updateValue,
|
||||
...divProps
|
||||
}) => {
|
||||
label = translate(label)
|
||||
disabledReason = translate(disabledReason)
|
||||
valueDisplay = typeof valueDisplay === 'string' ? translate(valueDisplay) : valueDisplay
|
||||
|
||||
const [value, setValue] = useState(valueProp)
|
||||
const getRatio = (v = value) => Math.max(Math.min((v - min) / (max - min), 1), 0)
|
||||
const [ratio, setRatio] = useState(getRatio())
|
||||
|
||||
// Throttling refs
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const lastValueRef = useRef<number>(valueProp)
|
||||
|
||||
useEffect(() => {
|
||||
setValue(valueProp)
|
||||
}, [valueProp])
|
||||
|
|
@ -42,14 +52,52 @@ const Slider: React.FC<Props> = ({
|
|||
setRatio(getRatio())
|
||||
}, [value, min, max])
|
||||
|
||||
const fireValueUpdate = (dragEnd: boolean, v = value) => {
|
||||
const throttledUpdateValue = useCallback((newValue: number, dragEnd: boolean) => {
|
||||
if (updateOnDragEnd !== dragEnd) return
|
||||
updateValue?.(v)
|
||||
if (!updateValue) return
|
||||
|
||||
lastValueRef.current = newValue
|
||||
|
||||
if (!throttle) {
|
||||
// No throttling
|
||||
updateValue(newValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
|
||||
// Set new timeout
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
updateValue(lastValueRef.current)
|
||||
timeoutRef.current = null
|
||||
}, throttle)
|
||||
}, [updateValue, updateOnDragEnd, throttle])
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
// Fire the last value immediately on cleanup
|
||||
if (updateValue && lastValueRef.current !== undefined) {
|
||||
updateValue(lastValueRef.current)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [updateValue])
|
||||
|
||||
const fireValueUpdate = (dragEnd: boolean, v = value) => {
|
||||
throttledUpdateValue(v, dragEnd)
|
||||
}
|
||||
|
||||
const labelText = `${label}: ${valueDisplay ?? value} ${unit}`
|
||||
|
||||
return (
|
||||
<SharedHudVars>
|
||||
<div className={styles['slider-container']} style={{ width }} {...divProps}>
|
||||
<div className={`${styles['slider-container']} settings-text-container ${labelText.length > 17 ? 'settings-text-container-long' : ''}`} style={{ width }} {...divProps}>
|
||||
<input
|
||||
type="range"
|
||||
className={styles.slider}
|
||||
|
|
@ -76,7 +124,7 @@ const Slider: React.FC<Props> = ({
|
|||
<div className={styles.disabled} title={disabledReason} />
|
||||
<div className={styles['slider-thumb']} style={{ left: `calc((100% * ${ratio}) - (8px * ${ratio}))` }} />
|
||||
<label className={styles.label}>
|
||||
{label}: {valueDisplay ?? value} {unit}
|
||||
{labelText}
|
||||
</label>
|
||||
</div>
|
||||
</SharedHudVars>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { showNotification } from '../react/NotificationProvider'
|
|||
import { pixelartIcons } from '../react/PixelartIcon'
|
||||
import { createSoundMap, SoundMap } from './soundsMap'
|
||||
import { musicSystem } from './musicSystem'
|
||||
import './customSoundSystem'
|
||||
|
||||
let soundMap: SoundMap | undefined
|
||||
|
||||
|
|
@ -50,8 +51,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
appViewer.backend?.soundSystem?.playSound(
|
||||
position,
|
||||
soundData.url,
|
||||
soundData.volume * (options.volume / 100),
|
||||
Math.max(Math.min(pitch ?? 1, 2), 0.5)
|
||||
soundData.volume,
|
||||
Math.max(Math.min(pitch ?? 1, 2), 0.5),
|
||||
soundData.timeout ?? options.remoteSoundsLoadTimeout
|
||||
)
|
||||
}
|
||||
if (getDistance(bot.entity.position, position) < 4 * 16) {
|
||||
|
|
@ -81,7 +83,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
const randomMusicKey = musicKeys[Math.floor(Math.random() * musicKeys.length)]
|
||||
const soundData = await soundMap.getSoundUrl(randomMusicKey)
|
||||
if (!soundData) return
|
||||
if (!soundData || !soundMap) return
|
||||
await musicSystem.playMusic(soundData.url, soundData.volume)
|
||||
}
|
||||
|
||||
|
|
@ -109,6 +111,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
|
||||
bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => {
|
||||
if (/^https?:/.test(soundId.replace('minecraft:', ''))) {
|
||||
return
|
||||
}
|
||||
await playHardcodedSound(soundId, position, volume, pitch)
|
||||
})
|
||||
|
||||
|
|
|
|||
44
src/sounds/customSoundSystem.ts
Normal file
44
src/sounds/customSoundSystem.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { loadOrPlaySound, stopAllSounds, stopSound } from '../basicSounds'
|
||||
|
||||
const customSoundSystem = () => {
|
||||
bot._client.on('named_sound_effect', packet => {
|
||||
let { soundName } = packet
|
||||
let metadata = {} as { loadTimeout?: number, loop?: boolean }
|
||||
|
||||
// Extract JSON metadata from parentheses at the end
|
||||
const jsonMatch = /\(({.*})\)$/.exec(soundName)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
metadata = JSON.parse(jsonMatch[1])
|
||||
soundName = soundName.slice(0, -jsonMatch[0].length)
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse sound metadata:', jsonMatch[1])
|
||||
}
|
||||
}
|
||||
|
||||
if (/^https?:/.test(soundName.replace('minecraft:', ''))) {
|
||||
const { loadTimeout, loop } = metadata
|
||||
void loadOrPlaySound(soundName, packet.volume, loadTimeout, loop)
|
||||
}
|
||||
})
|
||||
|
||||
bot._client.on('stop_sound', packet => {
|
||||
const { flags, source, sound } = packet
|
||||
|
||||
if (flags === 0) {
|
||||
// Stop all sounds
|
||||
stopAllSounds()
|
||||
} else if (sound) {
|
||||
// Stop specific sound by name
|
||||
stopSound(sound)
|
||||
}
|
||||
})
|
||||
|
||||
bot.on('end', () => {
|
||||
stopAllSounds()
|
||||
})
|
||||
}
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
customSoundSystem()
|
||||
})
|
||||
|
|
@ -35,6 +35,7 @@ interface ResourcePackSoundEntry {
|
|||
name: string
|
||||
stream?: boolean
|
||||
volume?: number
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface ResourcePackSound {
|
||||
|
|
@ -140,7 +141,7 @@ export class SoundMap {
|
|||
await scan(soundsBasePath)
|
||||
}
|
||||
|
||||
async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> {
|
||||
async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number, timeout?: number } | undefined> {
|
||||
// First check resource pack sounds.json
|
||||
if (this.activeResourcePackSoundsJson && soundKey in this.activeResourcePackSoundsJson) {
|
||||
const rpSound = this.activeResourcePackSoundsJson[soundKey]
|
||||
|
|
@ -151,6 +152,13 @@ export class SoundMap {
|
|||
if (this.activeResourcePackBasePath) {
|
||||
const tryFormat = async (format: string) => {
|
||||
try {
|
||||
if (sound.name.startsWith('http://') || sound.name.startsWith('https://')) {
|
||||
return {
|
||||
url: sound.name,
|
||||
volume: soundVolume * Math.max(Math.min(volume, 1), 0),
|
||||
timeout: sound.timeout
|
||||
}
|
||||
}
|
||||
const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.name}.${format}`)
|
||||
const fileData = await fs.promises.readFile(resourcePackPath)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@ export const watchOptionsAfterViewerInit = () => {
|
|||
updateFpsLimit(o)
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
appViewer.inWorldRenderingConfig.volume = Math.max(o.volume / 100, 0)
|
||||
})
|
||||
|
||||
watchValue(options, o => {
|
||||
appViewer.inWorldRenderingConfig.vrSupport = o.vrSupport
|
||||
appViewer.inWorldRenderingConfig.vrPageGameRendering = o.vrPageGameRendering
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue