ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
273 lines (272 loc) • 9.38 kB
TypeScript
/**
* 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>;