Feature/Sound, mangling and minification #6

Merged
jusemon merged 4 commits from Feature/Sound-mangling-and-minification into main 2026-01-07 00:04:17 -05:00
16 changed files with 739 additions and 14 deletions
Showing only changes of commit 2213f64e60 - Show all commits

88
src/components/Music.ts Normal file
View file

@ -0,0 +1,88 @@
import { Component } from '../core/Component.ts';
import { ComponentType } from '../core/Constants.ts';
import type { Sequence } from '../core/Music.ts';
/**
* Component for managing background music and sound effects.
*/
export class Music extends Component {
sequences: Map<string, Sequence>;
currentSequence: Sequence | null;
volume: number;
enabled: boolean;
constructor() {
super(ComponentType.MUSIC);
this.sequences = new Map();
this.currentSequence = null;
this.volume = 0.5;
this.enabled = true;
}
/**
* Add a music sequence.
* @param name - Unique identifier for the sequence
* @param sequence - The sequence instance
*/
addSequence(name: string, sequence: Sequence): void {
this.sequences.set(name, sequence);
if (sequence.gain) {
sequence.gain.gain.value = this.volume;
}
}
/**
* Play a sequence by name.
* @param name - The sequence identifier
*/
playSequence(name: string): void {
if (!this.enabled) return;
const sequence = this.sequences.get(name);
if (sequence) {
this.stop();
this.currentSequence = sequence;
if (sequence.gain) {
sequence.gain.gain.value = this.volume;
}
sequence.play();
}
}
/**
* Stop current playback.
*/
stop(): void {
if (this.currentSequence) {
this.currentSequence.stop();
this.currentSequence = null;
}
}
/**
* Set the volume (0.0 to 1.0).
* @param volume - Volume level
*/
setVolume(volume: number): void {
this.volume = Math.max(0, Math.min(1, volume));
if (this.currentSequence && this.currentSequence.gain) {
this.currentSequence.gain.gain.value = this.volume;
}
this.sequences.forEach((seq) => {
if (seq.gain) {
seq.gain.gain.value = this.volume;
}
});
}
/**
* Enable or disable music playback.
* @param enabled - Whether music should be enabled
*/
setEnabled(enabled: boolean): void {
this.enabled = enabled;
if (!enabled) {
this.stop();
}
}
}

View file

@ -0,0 +1,73 @@
import { Component } from '../core/Component.ts';
import { ComponentType } from '../core/Constants.ts';
import type { Sequence } from '../core/Music.ts';
/**
* Component for managing sound effects.
* Sound effects are short, one-shot audio sequences.
*/
export class SoundEffects extends Component {
sounds: Map<string, Sequence>;
volume: number;
enabled: boolean;
audioContext: AudioContext | null;
constructor(audioContext?: AudioContext) {
super(ComponentType.SOUND_EFFECTS);
this.sounds = new Map();
this.volume = 0.15; // Reduced default volume
this.enabled = true;
this.audioContext = audioContext || null;
}
/**
* Add a sound effect sequence.
* @param name - Unique identifier for the sound
* @param sequence - The sequence instance (should be short, non-looping)
*/
addSound(name: string, sequence: Sequence): void {
sequence.loop = false; // SFX should never loop
this.sounds.set(name, sequence);
if (sequence.gain) {
sequence.gain.gain.value = this.volume;
}
}
/**
* Play a sound effect by name.
* @param name - The sound identifier
*/
play(name: string): void {
if (!this.enabled) return;
const sound = this.sounds.get(name);
if (sound) {
sound.stop();
if (sound.gain) {
sound.gain.gain.value = this.volume;
}
sound.play();
}
}
/**
* Set the volume (0.0 to 1.0).
* @param volume - Volume level
*/
setVolume(volume: number): void {
this.volume = Math.max(0, Math.min(1, volume));
this.sounds.forEach((seq) => {
if (seq.gain) {
seq.gain.gain.value = this.volume;
}
});
}
/**
* Enable or disable sound effects.
* @param enabled - Whether sound effects should be enabled
*/
setEnabled(enabled: boolean): void {
this.enabled = enabled;
}
}

View file

@ -30,6 +30,8 @@ export enum ComponentType {
STEALTH = 'Stealth',
INTENT = 'Intent',
INVENTORY = 'Inventory',
MUSIC = 'Music',
SOUND_EFFECTS = 'SoundEffects',
}
/**
@ -79,4 +81,6 @@ export enum SystemName {
SKILL = 'SkillSystem',
STEALTH = 'StealthSystem',
HEALTH_REGEN = 'HealthRegenerationSystem',
MUSIC = 'MusicSystem',
SOUND_EFFECTS = 'SoundEffectsSystem',
}

View file

@ -11,7 +11,6 @@ export class Entity {
private components: Map<string, Component>;
active: boolean;
// Optional dynamic properties for specific entity types
owner?: number;
startX?: number;
startY?: number;

View file

@ -11,6 +11,9 @@ export enum Events {
SKILL_LEARNED = 'skills:learned',
ATTACK_PERFORMED = 'combat:attack_performed',
SKILL_COOLDOWN_STARTED = 'skills:cooldown_started',
ABSORPTION = 'absorption:absorbed',
PROJECTILE_CREATED = 'projectile:created',
PROJECTILE_IMPACT = 'projectile:impact',
}
/**

269
src/core/Music.ts Normal file
View file

@ -0,0 +1,269 @@
/**
* Note class - represents a single musical note
*/
export class Note {
frequency: number;
duration: number;
constructor(str: string) {
const couple = str.split(/\s+/);
this.frequency = Note.getFrequency(couple[0]) || 0;
this.duration = Note.getDuration(couple[1]) || 0;
}
/**
* Convert a note name (e.g. 'A4') to a frequency (e.g. 440.00)
*/
static getFrequency(name: string): number {
const enharmonics = 'B#-C|C#-Db|D|D#-Eb|E-Fb|E#-F|F#-Gb|G|G#-Ab|A|A#-Bb|B-Cb';
const middleC = 440 * Math.pow(Math.pow(2, 1 / 12), -9);
const octaveOffset = 4;
const num = /(\d+)/;
const offsets: Record<string, number> = {};
enharmonics.split('|').forEach((val, i) => {
val.split('-').forEach((note) => {
offsets[note] = i;
});
});
const couple = name.split(num);
const distance = offsets[couple[0]] ?? 0;
const octaveDiff = parseInt(couple[1] || String(octaveOffset), 10) - octaveOffset;
const freq = middleC * Math.pow(Math.pow(2, 1 / 12), distance);
return freq * Math.pow(2, octaveDiff);
}
/**
* Convert a duration string (e.g. 'q') to a number (e.g. 1)
*/
static getDuration(symbol: string): number {
const numeric = /^[0-9.]+$/;
if (numeric.test(symbol)) {
return parseFloat(symbol);
}
return symbol
.toLowerCase()
.split('')
.reduce((prev, curr) => {
return (
prev +
(curr === 'w'
? 4
: curr === 'h'
? 2
: curr === 'q'
? 1
: curr === 'e'
? 0.5
: curr === 's'
? 0.25
: 0)
);
}, 0);
}
}
/**
* Sequence class - manages playback of musical sequences
*/
export class Sequence {
ac: AudioContext;
tempo: number;
loop: boolean;
smoothing: number;
staccato: number;
notes: Note[];
gain: GainNode;
bass: BiquadFilterNode | null;
mid: BiquadFilterNode | null;
treble: BiquadFilterNode | null;
waveType: OscillatorType | 'custom';
customWave?: [Float32Array, Float32Array];
osc: OscillatorNode | null;
constructor(ac?: AudioContext, tempo = 120, arr?: (Note | string)[]) {
this.ac = ac || new AudioContext();
this.tempo = tempo;
this.loop = true;
this.smoothing = 0;
this.staccato = 0;
this.notes = [];
this.bass = null;
this.mid = null;
this.treble = null;
this.osc = null;
this.waveType = 'square';
this.gain = this.ac.createGain();
this.createFxNodes();
if (arr) {
this.push(...arr);
}
}
/**
* Create gain and EQ nodes, then connect them
*/
createFxNodes(): void {
const eq: Array<[string, number]> = [
['bass', 100],
['mid', 1000],
['treble', 2500],
];
let prev: AudioNode = this.gain;
eq.forEach((config) => {
const filter = this.ac.createBiquadFilter();
filter.type = 'peaking';
filter.frequency.value = config[1];
prev.connect(filter);
prev = filter;
if (config[0] === 'bass') {
this.bass = filter;
} else if (config[0] === 'mid') {
this.mid = filter;
} else if (config[0] === 'treble') {
this.treble = filter;
}
});
prev.connect(this.ac.destination);
}
/**
* Accepts Note instances or strings (e.g. 'A4 e')
*/
push(...notes: (Note | string)[]): this {
notes.forEach((note) => {
this.notes.push(note instanceof Note ? note : new Note(note));
});
return this;
}
/**
* Create a custom waveform
*/
createCustomWave(real: number[], imag?: number[]): void {
if (!imag) {
imag = real;
}
this.waveType = 'custom';
this.customWave = [new Float32Array(real), new Float32Array(imag)];
}
/**
* Recreate the oscillator node (happens on every play)
*/
createOscillator(): this {
this.stop();
this.osc = this.ac.createOscillator();
if (this.customWave) {
this.osc.setPeriodicWave(this.ac.createPeriodicWave(this.customWave[0], this.customWave[1]));
} else {
this.osc.type = this.waveType === 'custom' ? 'square' : this.waveType;
}
if (this.gain) {
this.osc.connect(this.gain);
}
return this;
}
/**
* Schedule a note to play at the given time
*/
scheduleNote(index: number, when: number): number {
const duration = (60 / this.tempo) * this.notes[index].duration;
const cutoff = duration * (1 - (this.staccato || 0));
this.setFrequency(this.notes[index].frequency, when);
if (this.smoothing && this.notes[index].frequency) {
this.slide(index, when, cutoff);
}
this.setFrequency(0, when + cutoff);
return when + duration;
}
/**
* Get the next note
*/
getNextNote(index: number): Note {
return this.notes[index < this.notes.length - 1 ? index + 1 : 0];
}
/**
* How long do we wait before beginning the slide?
*/
getSlideStartDelay(duration: number): number {
return duration - Math.min(duration, (60 / this.tempo) * this.smoothing);
}
/**
* Slide the note at index into the next note
*/
slide(index: number, when: number, cutoff: number): this {
const next = this.getNextNote(index);
const start = this.getSlideStartDelay(cutoff);
this.setFrequency(this.notes[index].frequency, when + start);
this.rampFrequency(next.frequency, when + cutoff);
return this;
}
/**
* Set frequency at time
*/
setFrequency(freq: number, when: number): this {
if (this.osc) {
this.osc.frequency.setValueAtTime(freq, when);
}
return this;
}
/**
* Ramp to frequency at time
*/
rampFrequency(freq: number, when: number): this {
if (this.osc) {
this.osc.frequency.linearRampToValueAtTime(freq, when);
}
return this;
}
/**
* Run through all notes in the sequence and schedule them
*/
play(when?: number): this {
const startTime = typeof when === 'number' ? when : this.ac.currentTime;
this.createOscillator();
if (this.osc) {
this.osc.start(startTime);
let currentTime = startTime;
this.notes.forEach((_note, i) => {
currentTime = this.scheduleNote(i, currentTime);
});
this.osc.stop(currentTime);
this.osc.onended = this.loop ? () => this.play(currentTime) : null;
}
return this;
}
/**
* Stop playback
*/
stop(): this {
if (this.osc) {
this.osc.onended = null;
this.osc.disconnect();
this.osc = null;
}
return this;
}
}

View file

@ -81,7 +81,8 @@ export const PixelFont = {
const chars = text.toUpperCase().split('');
chars.forEach((char) => {
const glyph = FONT_DATA.get(char) || FONT_DATA.get('?')!;
const glyph = FONT_DATA.get(char) || FONT_DATA.get('?');
if (!glyph) return;
for (let row = 0; row < 7; row++) {
for (let col = 0; col < 5; col++) {
if ((glyph[row] >> (4 - col)) & 1) {

View file

@ -15,6 +15,8 @@ import { MenuSystem } from './systems/MenuSystem.ts';
import { RenderSystem } from './systems/RenderSystem.ts';
import { UISystem } from './systems/UISystem.ts';
import { VFXSystem } from './systems/VFXSystem.ts';
import { MusicSystem } from './systems/MusicSystem.ts';
import { SoundEffectsSystem } from './systems/SoundEffectsSystem.ts';
import { Position } from './components/Position.ts';
import { Velocity } from './components/Velocity.ts';
@ -30,8 +32,11 @@ import { AI } from './components/AI.ts';
import { Absorbable } from './components/Absorbable.ts';
import { SkillProgress } from './components/SkillProgress.ts';
import { Intent } from './components/Intent.ts';
import { Music } from './components/Music.ts';
import { SoundEffects } from './components/SoundEffects.ts';
import { EntityType, ComponentType } from './core/Constants.ts';
import { Sequence } from './core/Music.ts';
import type { Entity } from './core/Entity.ts';
const canvas = document.getElementById('game-canvas') as HTMLCanvasElement;
@ -42,6 +47,8 @@ if (!canvas) {
engine.addSystem(new MenuSystem(engine));
engine.addSystem(new InputSystem());
engine.addSystem(new MusicSystem());
engine.addSystem(new SoundEffectsSystem());
engine.addSystem(new PlayerControllerSystem());
engine.addSystem(new StealthSystem());
engine.addSystem(new AISystem());
@ -143,17 +150,121 @@ if (!canvas) {
}
}, 5000);
const musicSystem = engine.systems.find((s) => s.name === 'MusicSystem') as
| MusicSystem
| undefined;
if (musicSystem) {
const musicEntity = engine.createEntity();
const music = new Music();
const ac = musicSystem.getAudioContext();
const bgMusic = new Sequence(ac, 140, [
'C4 e',
'E4 e',
'G4 q',
'C5 e',
'G4 e',
'E4 q',
'A3 e',
'C4 e',
'E4 q',
'A4 e',
'E4 e',
'C4 q',
'F3 e',
'A3 e',
'C4 q',
'F4 e',
'C4 e',
'A3 q',
'G3 e',
'B3 e',
'D4 q',
'G4 e',
'D4 e',
'B3 q',
]);
bgMusic.loop = true;
bgMusic.staccato = 0.2;
bgMusic.smoothing = 0.4;
bgMusic.waveType = 'triangle';
music.addSequence('background', bgMusic);
music.setVolume(0.02);
musicEntity.addComponent(music);
canvas.addEventListener('click', () => {
musicSystem.resumeAudioContext();
if (music.enabled && !music.currentSequence) {
music.playSequence('background');
}
canvas.focus();
});
setTimeout(() => {
musicSystem.resumeAudioContext();
if (music.enabled) {
music.playSequence('background');
}
}, 1000);
const sfxSystem = engine.systems.find((s) => s.name === 'SoundEffectsSystem') as
| SoundEffectsSystem
| undefined;
if (sfxSystem) {
const sfxEntity = engine.createEntity();
const sfx = new SoundEffects(ac);
const attackSound = new Sequence(ac, 120, ['C5 s']);
attackSound.staccato = 0.8;
sfx.addSound('attack', attackSound);
const absorbSound = new Sequence(ac, 120, ['G4 e']);
absorbSound.staccato = 0.5;
sfx.addSound('absorb', absorbSound);
const skillSound = new Sequence(ac, 120, ['A4 e']);
skillSound.staccato = 0.6;
sfx.addSound('skill', skillSound);
const damageSound = new Sequence(ac, 120, ['F4 s']);
damageSound.staccato = 0.8;
sfx.addSound('damage', damageSound);
const shootSound = new Sequence(ac, 120, ['C5 s']);
shootSound.staccato = 0.9;
sfx.addSound('shoot', shootSound);
const impactSound = new Sequence(ac, 120, ['G4 s']);
impactSound.staccato = 0.7;
sfx.addSound('impact', impactSound);
sfx.setVolume(0.02);
sfxEntity.addComponent(sfx);
}
} else {
canvas.addEventListener('click', () => {
canvas.focus();
});
}
canvas.focus();
engine.start();
interface WindowWithGame {
gameEngine?: Engine;
player?: Entity;
music?: Music;
}
(window as WindowWithGame).gameEngine = engine;
(window as WindowWithGame).player = player;
canvas.addEventListener('click', () => {
canvas.focus();
});
if (musicSystem) {
const musicEntity = engine.getEntities().find((e) => e.hasComponent(ComponentType.MUSIC));
if (musicEntity) {
const music = musicEntity.getComponent<Music>(ComponentType.MUSIC);
if (music) {
(window as WindowWithGame).music = music;
}
}
}
}

View file

@ -1,5 +1,6 @@
import { Skill } from '../Skill.ts';
import { ComponentType, SystemName, EntityType } from '../../core/Constants.ts';
import { Events } from '../../core/EventBus.ts';
import { Position } from '../../components/Position.ts';
import { Velocity } from '../../components/Velocity.ts';
import { Sprite } from '../../components/Sprite.ts';
@ -89,6 +90,12 @@ export class SlimeGun extends Skill {
projectile.maxRange = this.range;
projectile.lifetime = this.range / this.speed + 1.0;
engine.emit(Events.PROJECTILE_CREATED, {
x: startX,
y: startY,
angle: shootAngle,
});
return true;
}
}

View file

@ -33,7 +33,8 @@ export class AISystem extends System {
const playerController = this.engine.systems.find(
(s) => s.name === SystemName.PLAYER_CONTROLLER
) as PlayerControllerSystem | undefined;
const player = playerController ? playerController.getPlayerEntity() : null;
const player = playerController?.getPlayerEntity();
if (!player) return;
const playerPos = player?.getComponent<Position>(ComponentType.POSITION);
const config = GameConfig.AI;

View file

@ -143,6 +143,8 @@ export class AbsorptionSystem extends System {
vfxSystem.createAbsorption(entityPos.x, entityPos.y);
}
}
this.engine.emit(Events.ABSORPTION, { entity });
}
/**

View file

@ -0,0 +1,59 @@
import { System } from '../core/System.ts';
import { SystemName, ComponentType } from '../core/Constants.ts';
import type { Entity } from '../core/Entity.ts';
import type { Engine } from '../core/Engine.ts';
import type { Music } from '../components/Music.ts';
/**
* System responsible for managing background music playback.
*/
export class MusicSystem extends System {
private audioContext: AudioContext | null;
constructor() {
super(SystemName.MUSIC);
this.requiredComponents = [ComponentType.MUSIC];
this.priority = 5;
this.audioContext = null;
}
/**
* Initialize the audio context when system is added to engine.
*/
init(engine: Engine): void {
super.init(engine);
}
/**
* Process music entities - currently just ensures audio context exists.
*/
process(_deltaTime: number, entities: Entity[]): void {
entities.forEach((entity) => {
const music = entity.getComponent<Music>(ComponentType.MUSIC);
if (!music) return;
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
});
}
/**
* Get or create the shared audio context.
*/
getAudioContext(): AudioContext {
if (!this.audioContext) {
this.audioContext = new AudioContext();
}
return this.audioContext;
}
/**
* Resume audio context (required after user interaction).
*/
resumeAudioContext(): void {
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
}
}

View file

@ -75,6 +75,12 @@ export class ProjectileSystem extends System {
if (targetHealthComp) {
targetHealthComp.takeDamage(damage);
this.engine.emit(Events.PROJECTILE_IMPACT, {
x: position.x,
y: position.y,
damage,
});
const vfxSystem = this.engine.systems.find((s) => s.name === SystemName.VFX) as
| VFXSystem
| undefined;

View file

@ -179,7 +179,6 @@ export class RenderSystem extends System {
let frames = spriteData[sprite.animationState as string] || spriteData[AnimationState.IDLE];
if (!frames || !Array.isArray(frames)) {
// Fallback to default slime animation if data structure is unexpected
frames = SpriteLibrary[EntityType.SLIME][AnimationState.IDLE];
}
@ -414,28 +413,24 @@ export class RenderSystem extends System {
*/
drawGlowEffect(sprite: Sprite): void {
const ctx = this.ctx;
const time = Date.now() * 0.001; // Time in seconds for pulsing
const pulse = 0.5 + Math.sin(time * 3) * 0.3; // Pulsing between 0.2 and 0.8
const time = Date.now() * 0.001;
const pulse = 0.5 + Math.sin(time * 3) * 0.3;
const baseRadius = Math.max(sprite.width, sprite.height) / 2;
const glowRadius = baseRadius + 4 + pulse * 2;
// Create radial gradient for soft glow
const gradient = ctx.createRadialGradient(0, 0, baseRadius, 0, 0, glowRadius);
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.4 * pulse})`);
gradient.addColorStop(0.5, `rgba(0, 230, 255, ${0.3 * pulse})`);
gradient.addColorStop(1, 'rgba(0, 230, 255, 0)');
// Draw multiple layers for a softer glow effect
ctx.save();
ctx.globalCompositeOperation = 'screen';
// Outer glow layer
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(0, 0, glowRadius, 0, Math.PI * 2);
ctx.fill();
// Inner bright core
const innerGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, baseRadius * 0.6);
innerGradient.addColorStop(0, `rgba(255, 255, 255, ${0.6 * pulse})`);
innerGradient.addColorStop(1, 'rgba(0, 230, 255, 0)');

View file

@ -1,6 +1,7 @@
import { System } from '../core/System.ts';
import { SkillRegistry } from '../skills/SkillRegistry.ts';
import { SystemName, ComponentType } from '../core/Constants.ts';
import { Events } from '../core/EventBus.ts';
import type { Entity } from '../core/Entity.ts';
import type { Skills } from '../components/Skills.ts';
import type { Intent } from '../components/Intent.ts';
@ -55,6 +56,7 @@ export class SkillSystem extends System {
if (skills) {
skills.setCooldown(skillId, skill.cooldown);
}
this.engine.emit(Events.SKILL_COOLDOWN_STARTED, { skillId });
}
}
}

View file

@ -0,0 +1,105 @@
import { System } from '../core/System.ts';
import { SystemName, ComponentType } from '../core/Constants.ts';
import { Events } from '../core/EventBus.ts';
import type { Entity } from '../core/Entity.ts';
import type { Engine } from '../core/Engine.ts';
import type { SoundEffects } from '../components/SoundEffects.ts';
import type { MusicSystem } from './MusicSystem.ts';
/**
* System responsible for managing sound effects playback.
* Follows ECS pattern: system processes entities with SoundEffects component.
* Listens to game events and plays appropriate sound effects.
*/
export class SoundEffectsSystem extends System {
private sfxEntity: Entity | null;
constructor() {
super(SystemName.SOUND_EFFECTS);
this.requiredComponents = [ComponentType.SOUND_EFFECTS];
this.priority = 5;
this.sfxEntity = null;
}
/**
* Initialize event listeners when system is added to engine.
*/
init(engine: Engine): void {
super.init(engine);
this.engine.on(Events.ATTACK_PERFORMED, () => {
this.playSound('attack');
});
this.engine.on(Events.DAMAGE_DEALT, () => {
this.playSound('damage');
});
this.engine.on(Events.ABSORPTION, () => {
this.playSound('absorb');
});
this.engine.on(Events.SKILL_LEARNED, () => {
this.playSound('skill');
});
this.engine.on(Events.SKILL_COOLDOWN_STARTED, () => {
this.playSound('skill');
});
this.engine.on(Events.PROJECTILE_CREATED, () => {
this.playSound('shoot');
});
this.engine.on(Events.PROJECTILE_IMPACT, () => {
this.playSound('impact');
});
}
/**
* Process sound effect entities - ensures audio context is available.
*/
process(_deltaTime: number, entities: Entity[]): void {
entities.forEach((entity) => {
const sfx = entity.getComponent<SoundEffects>(ComponentType.SOUND_EFFECTS);
if (!sfx) return;
if (!this.sfxEntity) {
this.sfxEntity = entity;
}
if (!sfx.audioContext) {
const musicSystem = this.engine.systems.find((s) => s.name === SystemName.MUSIC) as
| MusicSystem
| undefined;
if (musicSystem) {
sfx.audioContext = musicSystem.getAudioContext();
}
}
});
}
/**
* Play a sound effect from any entity with SoundEffects component.
* @param soundName - The name of the sound to play
*/
playSound(soundName: string): void {
if (this.sfxEntity) {
const sfx = this.sfxEntity.getComponent<SoundEffects>(ComponentType.SOUND_EFFECTS);
if (sfx) {
sfx.play(soundName);
return;
}
}
const entities = this.engine.getEntities();
for (const entity of entities) {
const sfx = entity.getComponent<SoundEffects>(ComponentType.SOUND_EFFECTS);
if (sfx) {
this.sfxEntity = entity;
sfx.play(soundName);
break;
}
}
}
}