Wassim SAMAD bf9246cd36 feat(sfx): play paint_apply sound when a material is painted
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>
2026-06-19 10:50:48 -04:00

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',
)
}