UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

273 lines (272 loc) 9.38 kB
/** * Audio Plugin for ECSpresso * * Web Audio API integration via Howler.js for sound effects and music playback. * User-defined channels with type-safe volume control, hybrid resource + component API, * and asset manager integration. */ import { type BasePluginOptions } from 'ecspresso'; import type { AssetsOfWorld, AnyECSpresso, ChannelOfWorld } from 'ecspresso'; import type { Howl } from 'howler'; /** * Configuration for a single audio channel. */ export interface AudioChannelConfig { readonly volume: number; } /** * Define audio channels with type-safe names and initial volumes. * Mirrors `defineCollisionLayers` pattern. * * @param channels Object mapping channel names to their configuration * @returns Frozen channel configuration with inferred channel name union * * @example * ```typescript * const channels = defineAudioChannels({ * sfx: { volume: 1 }, * music: { volume: 0.7 }, * ui: { volume: 0.8 }, * }); * type Ch = ChannelsOf<typeof channels>; // 'sfx' | 'music' | 'ui' * ``` */ export declare function defineAudioChannels<const T extends Record<string, AudioChannelConfig>>(channels: T): Readonly<T>; /** * Extract channel name union from a `defineAudioChannels` result. */ export type ChannelsOf<T> = T extends Record<infer K extends string, AudioChannelConfig> ? K : never; /** * Audio source component attached to entities for positional/entity-bound audio. */ export interface AudioSource<Ch extends string = string> { /** Asset key for the sound */ readonly sound: string; /** Channel this sound plays on */ readonly channel: Ch; /** Individual volume (0-1) */ volume: number; /** Whether sound loops */ loop: boolean; /** Remove entity when sound ends (like timer autoRemove) */ autoRemove: boolean; /** Whether sound is currently playing (system-managed) */ playing: boolean; /** Howler sound ID (system-managed, -1 = not started) */ _soundId: number; } /** * Component types provided by the audio plugin. */ export interface AudioComponentTypes<Ch extends string = string> { audioSource: AudioSource<Ch>; } /** * Event to trigger fire-and-forget sound playback from any system. */ export interface PlaySoundEvent<Ch extends string = string> { /** Asset key for the sound */ sound: string; /** Channel to play on */ channel?: Ch; /** Individual volume (0-1) */ volume?: number; /** Whether sound loops */ loop?: boolean; } /** * Event to stop music on a channel. */ export interface StopMusicEvent<Ch extends string = string> { /** Channel to stop music on. If omitted, stops all music. */ channel?: Ch; } /** * Event published when a sound finishes playing. */ export interface SoundEndedEvent { /** Entity ID if sound was entity-attached, -1 for fire-and-forget */ entityId: number; /** Howler sound ID */ soundId: number; /** Asset key of the sound */ sound: string; } /** * Event types provided by the audio plugin. */ export interface AudioEventTypes<Ch extends string = string> { playSound: PlaySoundEvent<Ch>; stopMusic: StopMusicEvent<Ch>; soundEnded: SoundEndedEvent; } /** * Play options for fire-and-forget sound effects. */ export interface PlayOptions<Ch extends string = string> { /** Channel to play on (uses first defined channel if omitted) */ channel?: Ch; /** Individual volume (0-1, default: 1) */ volume?: number; /** Whether to loop (default: false) */ loop?: boolean; } /** * Music playback options. */ export interface MusicOptions<Ch extends string = string> { /** Channel to play music on (uses first defined channel if omitted) */ channel?: Ch; /** Volume (0-1, default: 1) */ volume?: number; /** Whether to loop (default: true) */ loop?: boolean; } /** * Audio state resource providing fire-and-forget SFX and music control. * Effective volume = individual * channel * master. */ export interface AudioState<Ch extends string = string> { /** Play a fire-and-forget sound effect. Returns the Howler sound ID. */ play(sound: string, options?: PlayOptions<Ch>): number; /** Stop a specific sound by its Howler sound ID. */ stop(soundId: number): void; /** Play music on a channel. Stops any existing music on that channel first. */ playMusic(sound: string, options?: MusicOptions<Ch>): void; /** Stop music on a channel. If omitted, stops all music. */ stopMusic(channel?: Ch): void; /** Pause music on a channel. If omitted, pauses all music. */ pauseMusic(channel?: Ch): void; /** Resume music on a channel. If omitted, resumes all music. */ resumeMusic(channel?: Ch): void; /** Set volume for a channel (0-1). */ setChannelVolume(channel: Ch, volume: number): void; /** Get current volume for a channel. */ getChannelVolume(channel: Ch): number; /** Set master volume (0-1). */ setMasterVolume(volume: number): void; /** Get current master volume. */ getMasterVolume(): number; /** Mute all audio. */ mute(): void; /** Unmute all audio. */ unmute(): void; /** Toggle mute state. */ toggleMute(): void; /** Check if audio is muted. */ isMuted(): boolean; } /** * Resource types provided by the audio plugin. */ export interface AudioResourceTypes<Ch extends string = string> { audioState: AudioState<Ch>; } /** * Configuration options for the audio plugin. */ export interface AudioPluginOptions<Ch extends string, G extends string = 'audio'> extends BasePluginOptions<G> { /** Channel definitions from defineAudioChannels */ channels: Readonly<Record<Ch, AudioChannelConfig>>; } /** * Create an audioSource component for entity-attached audio. * * @param sound Asset key for the sound * @param channel Channel to play on * @param options Optional configuration * @returns Component object suitable for spreading into spawn() * * @example * ```typescript * ecs.spawn({ * ...createAudioSource('explosion', 'sfx'), * ...createTransform(100, 200), * }); * ``` */ export declare function createAudioSource<Ch extends string>(sound: string, channel: Ch, options?: { volume?: number; loop?: boolean; autoRemove?: boolean; }): Pick<AudioComponentTypes<Ch>, 'audioSource'>; /** * Create a loader function for use with the asset manager. * Returns a factory function that loads a Howl when called. * * @param src URL(s) for the sound file * @param options Optional Howl configuration * @returns Factory function compatible with asset manager's loader parameter * * @example * ```typescript * const ecs = ECSpresso.create() * .withAssets(a => a * .add('explosion', loadSound('/sounds/explosion.mp3')) * .add('bgm', loadSound(['/sounds/bgm.webm', '/sounds/bgm.mp3'])) * ) * .build(); * ``` */ export declare function loadSound(src: string | string[], options?: { html5?: boolean; preload?: boolean; }): () => Promise<Howl>; /** * Create an audio plugin for ECSpresso. * * Provides: * - `audioState` resource for fire-and-forget SFX and music * - `audioSource` component for entity-attached sounds * - Volume hierarchy: individual * channel * master * - `playSound` / `stopMusic` event handlers * - `soundEnded` event on completion * - Automatic cleanup on entity removal (dispose callback) * * Sounds must be preloaded through the asset pipeline (`loadSound` helper). * * @example * ```typescript * const channels = defineAudioChannels({ * sfx: { volume: 1 }, * music: { volume: 0.7 }, * }); * * const ecs = ECSpresso.create() * .withAssets(a => a.add('explosion', loadSound('/sfx/boom.mp3'))) * .withPlugin(createAudioPlugin({ channels })) * .build(); * * await ecs.initialize(); * const audio = ecs.getResource('audioState'); * audio.play('explosion', { channel: 'sfx' }); * ``` */ export declare function createAudioPlugin<Ch extends string, G extends string = 'audio'>(options: AudioPluginOptions<Ch, G>): import("ecspresso").Plugin<import("ecspresso").WithResources<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, AudioComponentTypes<Ch>>, AudioEventTypes<Ch>>, AudioResourceTypes<Ch>>, import("ecspresso").EmptyConfig, "audio-sync", G, never, "audio-sources">; /** * Typed helpers for the audio plugin. * Creates helpers that validate sound keys and channel names against the world type W. * Call after .build() using typeof ecs. * * @template W - Concrete ECS world type (e.g. `typeof ecs`) * * @example * ```typescript * const ecs = ECSpresso.create() * .withPlugin(createAudioPlugin({ channels })) * .withAssets(a => a.add('boom', loadSound('/sfx/boom.mp3'))) * .build(); * * const { createAudioSource } = createAudioHelpers<typeof ecs>(); * // Type-safe: 'boom' must be a registered asset, 'sfx' a valid channel * createAudioSource('boom', 'sfx'); * ``` */ export interface AudioHelpers<W extends AnyECSpresso> { createAudioSource: (sound: keyof AssetsOfWorld<W> & string, channel: ChannelOfWorld<W>, options?: { volume?: number; loop?: boolean; autoRemove?: boolean; }) => Pick<AudioComponentTypes<ChannelOfWorld<W>>, 'audioSource'>; } export declare function createAudioHelpers<W extends AnyECSpresso>(_world?: W): AudioHelpers<W>;