Registers a paintApply SFX (with rate/volume jitter + a 60ms gap so rapid multi-face painting doesn't machine-gun), wires the sfx:paint-apply bus event, and emits it from the material-paint click chokepoint in the selection manager (fires on apply, not hover/preview). Adds the audio asset to the editor app's public resources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import mitt from 'mitt'
|
|
import { playSFX } from './sfx-player'
|
|
|
|
/**
|
|
* SFX-specific events that tools can trigger
|
|
*/
|
|
type SFXEvents = {
|
|
'sfx:grid-snap': undefined
|
|
'sfx:item-delete': undefined
|
|
'sfx:item-pick': undefined
|
|
'sfx:item-place': undefined
|
|
'sfx:item-rotate': undefined
|
|
'sfx:structure-build-start': undefined
|
|
'sfx:structure-build': undefined
|
|
'sfx:structure-delete': undefined
|
|
'sfx:snapshot-capture': undefined
|
|
'sfx:menu-hover': undefined
|
|
'sfx:menu-click': undefined
|
|
'sfx:paint-apply': undefined
|
|
}
|
|
|
|
/**
|
|
* Dedicated event emitter for SFX
|
|
* Tools should use this to trigger sound effects
|
|
*/
|
|
export const sfxEmitter = mitt<SFXEvents>()
|
|
|
|
let sfxBusInitialized = false
|
|
|
|
/**
|
|
* Initialize SFX Bus - connects SFX events to actual sound playback.
|
|
* Safe to call multiple times; re-registration is a no-op once initialized.
|
|
*/
|
|
export function initSFXBus() {
|
|
if (sfxBusInitialized) return
|
|
sfxBusInitialized = true
|
|
// Map SFX events to sound playback
|
|
sfxEmitter.on('sfx:grid-snap', () => playSFX('gridSnap'))
|
|
sfxEmitter.on('sfx:item-delete', () => playSFX('itemDelete'))
|
|
sfxEmitter.on('sfx:item-pick', () => playSFX('itemPick'))
|
|
sfxEmitter.on('sfx:item-place', () => playSFX('itemPlace'))
|
|
sfxEmitter.on('sfx:item-rotate', () => playSFX('itemRotate'))
|
|
sfxEmitter.on('sfx:structure-build-start', () => playSFX('structureBuildStart'))
|
|
sfxEmitter.on('sfx:structure-build', () => playSFX('structureBuildEnd'))
|
|
sfxEmitter.on('sfx:structure-delete', () => playSFX('structureDelete'))
|
|
sfxEmitter.on('sfx:snapshot-capture', () => playSFX('snapshotCapture'))
|
|
sfxEmitter.on('sfx:menu-hover', () => playSFX('menuHover'))
|
|
sfxEmitter.on('sfx:menu-click', () => playSFX('menuClick'))
|
|
sfxEmitter.on('sfx:paint-apply', () => playSFX('paintApply'))
|
|
}
|
|
|
|
/**
|
|
* Helper function to trigger SFX events from tools
|
|
* @example
|
|
* triggerSFX('sfx:item-place')
|
|
*/
|
|
export function triggerSFX(event: keyof SFXEvents) {
|
|
sfxEmitter.emit(event)
|
|
}
|
|
|
|
/**
|
|
* Node types whose deletion should use the lighter item-delete cue rather
|
|
* than the heavier structure-delete one. Shelves are furniture-like placeable
|
|
* objects, so they sound like items being removed, not structures demolished.
|
|
*/
|
|
const ITEM_DELETE_NODE_TYPES = new Set(['item', 'shelf'])
|
|
|
|
/**
|
|
* Emit the delete SFX appropriate for a deleted node's type.
|
|
*/
|
|
export function emitDeleteSFX(nodeType: string | undefined) {
|
|
sfxEmitter.emit(
|
|
nodeType && ITEM_DELETE_NODE_TYPES.has(nodeType) ? 'sfx:item-delete' : 'sfx:structure-delete',
|
|
)
|
|
}
|