diff --git a/src/client/controllers/SoundController.ts b/src/client/controllers/SoundController.ts index 2d66f570d..785805e65 100644 --- a/src/client/controllers/SoundController.ts +++ b/src/client/controllers/SoundController.ts @@ -7,11 +7,7 @@ import { } from "../../core/game/GameUpdates"; import { GameView } from "../../core/game/GameView"; import { Controller } from "../Controller"; -import { - PlaySoundEffectEvent, - SoundEffect, - SoundUpdateEvent, -} from "../sound/Sounds"; +import { PlaySoundEffectEvent, SoundUpdateEvent } from "../sound/Sounds"; export class SoundController implements Controller { constructor( @@ -34,7 +30,7 @@ export class SoundController implements Controller { gu.updates[GameUpdateType.ConquestEvent]?.forEach( (cu: ConquestUpdate) => { if (cu.conquerorId === myPlayer.id()) { - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.KaChing)); + this.eventBus.emit(new PlaySoundEffectEvent("ka-ching")); } }, ); @@ -56,52 +52,42 @@ export class SoundController implements Controller { private handleNewUnitSounds(unitType: UnitType, isMine: boolean) { switch (unitType) { case UnitType.AtomBomb: - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AtomLaunch)); + this.eventBus.emit(new PlaySoundEffectEvent("atom-launch")); break; case UnitType.HydrogenBomb: - this.eventBus.emit( - new PlaySoundEffectEvent(SoundEffect.HydrogenLaunch), - ); + this.eventBus.emit(new PlaySoundEffectEvent("hydrogen-launch")); break; case UnitType.MIRV: - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.MirvLaunch)); + this.eventBus.emit(new PlaySoundEffectEvent("mirv-launch")); break; case UnitType.Warship: if (isMine) - this.eventBus.emit( - new PlaySoundEffectEvent(SoundEffect.BuildWarship), - ); + this.eventBus.emit(new PlaySoundEffectEvent("build-warship")); break; case UnitType.City: - if (isMine) - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.BuildCity)); + if (isMine) this.eventBus.emit(new PlaySoundEffectEvent("build-city")); break; case UnitType.Port: - if (isMine) - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.BuildPort)); + if (isMine) this.eventBus.emit(new PlaySoundEffectEvent("build-port")); break; case UnitType.DefensePost: if (isMine) - this.eventBus.emit( - new PlaySoundEffectEvent(SoundEffect.BuildDefensePost), - ); + this.eventBus.emit(new PlaySoundEffectEvent("build-defense-post")); break; case UnitType.SAMLauncher: - if (isMine) - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.SamBuilt)); + if (isMine) this.eventBus.emit(new PlaySoundEffectEvent("sam-built")); break; case UnitType.MissileSilo: - if (isMine) - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.SiloBuilt)); + if (isMine) this.eventBus.emit(new PlaySoundEffectEvent("silo-built")); break; } } private handleImpactSounds(unitType: UnitType) { if (unitType === UnitType.HydrogenBomb) { - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.HydrogenHit)); + this.eventBus.emit(new PlaySoundEffectEvent("hydrogen-hit")); } else if (unitType === UnitType.AtomBomb || unitType === UnitType.MIRV) { - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AtomHit)); + this.eventBus.emit(new PlaySoundEffectEvent("atom-hit")); } } } diff --git a/src/client/hud/layers/ActionableEvents.ts b/src/client/hud/layers/ActionableEvents.ts index 59b96e363..c5831722d 100644 --- a/src/client/hud/layers/ActionableEvents.ts +++ b/src/client/hud/layers/ActionableEvents.ts @@ -10,7 +10,7 @@ import { } from "../../../core/game/GameUpdates"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { Controller } from "../../Controller"; -import { PlaySoundEffectEvent, SoundEffect } from "../../sound/Sounds"; +import { PlaySoundEffectEvent } from "../../sound/Sounds"; import { GoToPlayerEvent } from "../../TransformHandler"; import { SendAllianceExtensionIntentEvent, @@ -203,9 +203,7 @@ export class ActionableEvents extends LitElement implements Controller { ) as PlayerView; if (!requestor.isAlliedWith(recipient)) { - this.eventBus.emit( - new PlaySoundEffectEvent(SoundEffect.AllianceSuggested), - ); + this.eventBus.emit(new PlaySoundEffectEvent("alliance-suggested")); } this.addEvent({ description: translateText("events_display.request_alliance", { diff --git a/src/client/hud/layers/EventsDisplay.ts b/src/client/hud/layers/EventsDisplay.ts index 6a6ef60dc..caadf642a 100644 --- a/src/client/hud/layers/EventsDisplay.ts +++ b/src/client/hud/layers/EventsDisplay.ts @@ -23,7 +23,7 @@ import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { onlyImages } from "../../../core/Util"; import { GoToPlayerEvent, GoToUnitEvent } from "../../TransformHandler"; -import { PlaySoundEffectEvent, SoundEffect } from "../../sound/Sounds"; +import { PlaySoundEffectEvent } from "../../sound/Sounds"; import { UIState } from "../../UIState"; import { getMessageTypeClasses, @@ -297,7 +297,7 @@ export class EventsDisplay extends LitElement implements Controller { type: MessageType.CHAT, unsafeDescription: false, }); - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Message)); + this.eventBus.emit(new PlaySoundEffectEvent("message")); } onAllianceRequestReplyEvent(update: AllianceRequestReplyUpdate) { @@ -335,7 +335,7 @@ export class EventsDisplay extends LitElement implements Controller { if (betrayed.isDisconnected()) return; // Do not send the message if betraying a disconnected player if (!betrayed.isTraitor() && traitor === myPlayer) { - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AllianceBroken)); + this.eventBus.emit(new PlaySoundEffectEvent("alliance-broken")); const malusPercent = Math.round( (1 - this.game.config().traitorDefenseDebuff()) * 100, ); @@ -362,7 +362,7 @@ export class EventsDisplay extends LitElement implements Controller { focusID: update.betrayedID, }); } else if (betrayed === myPlayer) { - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AllianceBroken)); + this.eventBus.emit(new PlaySoundEffectEvent("alliance-broken")); this.addEvent({ description: translateText("events_display.betrayed_you", { name: traitor.displayName(), diff --git a/src/client/hud/layers/RadialMenu.ts b/src/client/hud/layers/RadialMenu.ts index 3005e568f..740a0b90e 100644 --- a/src/client/hud/layers/RadialMenu.ts +++ b/src/client/hud/layers/RadialMenu.ts @@ -3,7 +3,7 @@ import { assetUrl } from "../../../core/AssetUrls"; import { EventBus, GameEvent } from "../../../core/EventBus"; import { Controller } from "../../Controller"; import { CloseViewEvent } from "../../InputHandler"; -import { PlaySoundEffectEvent, SoundEffect } from "../../sound/Sounds"; +import { PlaySoundEffectEvent } from "../../sound/Sounds"; import { getSvgAspectRatio, translateText } from "../../Utils"; import { CenterButtonElement, @@ -507,7 +507,7 @@ export class RadialMenu implements Controller { this.navigationInProgress ) return; - this.eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + this.eventBus.emit(new PlaySoundEffectEvent("click")); if ( this.currentLevel > 0 && diff --git a/src/client/sound/Sounds.ts b/src/client/sound/Sounds.ts index 34ab8fc4a..59ef6fe87 100644 --- a/src/client/sound/Sounds.ts +++ b/src/client/sound/Sounds.ts @@ -2,48 +2,41 @@ import { assetUrl } from "../../core/AssetUrls"; import { GameEvent } from "../../core/EventBus"; import { GameUpdateViewData } from "../../core/game/GameUpdates"; -export enum SoundEffect { - KaChing = "ka-ching", - AtomHit = "atom-hit", - AtomLaunch = "atom-launch", - HydrogenHit = "hydrogen-hit", - HydrogenLaunch = "hydrogen-launch", - MirvLaunch = "mirv-launch", - AllianceSuggested = "alliance-suggested", - AllianceBroken = "alliance-broken", - BuildPort = "build-port", - BuildCity = "build-city", - BuildDefensePost = "build-defense-post", - BuildWarship = "build-warship", - SamBuilt = "sam-built", - SiloBuilt = "silo-built", - Message = "message", - Click = "click", -} +export type SoundEffect = + | "ka-ching" + | "atom-hit" + | "atom-launch" + | "hydrogen-hit" + | "hydrogen-launch" + | "mirv-launch" + | "alliance-suggested" + | "alliance-broken" + | "build-port" + | "build-city" + | "build-defense-post" + | "build-warship" + | "sam-built" + | "silo-built" + | "message" + | "click"; export const soundEffectUrls: ReadonlyMap = new Map([ - [SoundEffect.KaChing, assetUrl("sounds/effects/ka-ching.mp3")], - [SoundEffect.AtomHit, assetUrl("sounds/effects/atom-hit.mp3")], - [SoundEffect.AtomLaunch, assetUrl("sounds/effects/atom-launch.mp3")], - [SoundEffect.HydrogenHit, assetUrl("sounds/effects/hydrogen-hit.mp3")], - [SoundEffect.HydrogenLaunch, assetUrl("sounds/effects/hydrogen-launch.mp3")], - [SoundEffect.MirvLaunch, assetUrl("sounds/effects/mirv-launch.mp3")], - [ - SoundEffect.AllianceSuggested, - assetUrl("sounds/effects/alliance-suggested.mp3"), - ], - [SoundEffect.AllianceBroken, assetUrl("sounds/effects/alliance-broken.mp3")], - [SoundEffect.BuildPort, assetUrl("sounds/effects/build-port.mp3")], - [SoundEffect.BuildCity, assetUrl("sounds/effects/build-city.mp3")], - [ - SoundEffect.BuildDefensePost, - assetUrl("sounds/effects/build-defense-post.mp3"), - ], - [SoundEffect.BuildWarship, assetUrl("sounds/effects/build-warship.mp3")], - [SoundEffect.SamBuilt, assetUrl("sounds/effects/sam-built.mp3")], - [SoundEffect.SiloBuilt, assetUrl("sounds/effects/silo-built.mp3")], - [SoundEffect.Message, assetUrl("sounds/effects/message.mp3")], - [SoundEffect.Click, assetUrl("sounds/effects/click.mp3")], + ["ka-ching", assetUrl("sounds/effects/ka-ching.mp3")], + ["atom-hit", assetUrl("sounds/effects/atom-hit.mp3")], + ["atom-launch", assetUrl("sounds/effects/atom-launch.mp3")], + ["hydrogen-hit", assetUrl("sounds/effects/hydrogen-hit.mp3")], + ["hydrogen-launch", assetUrl("sounds/effects/hydrogen-launch.mp3")], + ["mirv-launch", assetUrl("sounds/effects/mirv-launch.mp3")], + ["alliance-suggested", assetUrl("sounds/effects/alliance-suggested.mp3")], + ["alliance-broken", assetUrl("sounds/effects/alliance-broken.mp3")], + ["build-port", assetUrl("sounds/effects/build-port.mp3")], + ["build-city", assetUrl("sounds/effects/build-city.mp3")], + ["build-defense-post", assetUrl("sounds/effects/build-defense-post.mp3")], + ["build-warship", assetUrl("sounds/effects/build-warship.mp3")], + ["sam-built", assetUrl("sounds/effects/sam-built.mp3")], + ["silo-built", assetUrl("sounds/effects/silo-built.mp3")], + ["message", assetUrl("sounds/effects/message.mp3")], + ["click", assetUrl("sounds/effects/click.mp3")], ]); export class PlaySoundEffectEvent implements GameEvent { diff --git a/tests/client/sound/SoundManager.test.ts b/tests/client/sound/SoundManager.test.ts index e9b0d49fd..a8827ebf1 100644 --- a/tests/client/sound/SoundManager.test.ts +++ b/tests/client/sound/SoundManager.test.ts @@ -67,7 +67,6 @@ import { PlaySoundEffectEvent, SetBackgroundMusicVolumeEvent, SetSoundEffectsVolumeEvent, - SoundEffect, } from "../../../src/client/sound/Sounds"; import { EventBus } from "../../../src/core/EventBus"; import { UserSettings } from "../../../src/core/game/UserSettings"; @@ -94,14 +93,14 @@ describe("SoundManager", () => { }); it("lazy-loads a sound effect once and reuses it", () => { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); + eventBus.emit(new PlaySoundEffectEvent("click")); // 3 background music Howls + 1 Click Howl = 4 expect(howlCtor).toHaveBeenCalledTimes(4); }); it("plays a sound effect when PlaySoundEffectEvent is emitted", () => { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AtomHit)); + eventBus.emit(new PlaySoundEffectEvent("atom-hit")); const effectHowl = howlInstances[howlInstances.length - 1]; expect(effectHowl.play).toHaveBeenCalledTimes(1); }); @@ -124,7 +123,7 @@ describe("SoundManager", () => { howlCtor.mockClear(); howlInstances.length = 0; new SoundManager(bus, settings); - bus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + bus.emit(new PlaySoundEffectEvent("click")); expect(howlCtor).toHaveBeenLastCalledWith( expect.objectContaining({ volume: 0.3 }), ); @@ -139,7 +138,7 @@ describe("SoundManager", () => { }); it("responds to SetSoundEffectsVolumeEvent", () => { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); const clickHowl = howlInstances[howlInstances.length - 1]; clickHowl.volume.mockClear(); eventBus.emit(new SetSoundEffectsVolumeEvent(0.4)); @@ -161,18 +160,18 @@ describe("SoundManager", () => { }); it("dispose() unsubscribes from EventBus so events no longer play sounds", () => { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); const clickHowl = howlInstances[howlInstances.length - 1]; expect(clickHowl.play).toHaveBeenCalledTimes(1); soundManager.dispose(); - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); expect(clickHowl.play).toHaveBeenCalledTimes(1); }); it("dispose() stops and unloads all loaded sound effects", () => { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); const clickHowl = howlInstances[howlInstances.length - 1]; soundManager.dispose(); @@ -193,7 +192,7 @@ describe("SoundManager", () => { }); it("does not throw when playSoundEffect is called directly", () => { - expect(() => soundManager.playSoundEffect(SoundEffect.Click)).not.toThrow(); + expect(() => soundManager.playSoundEffect("click")).not.toThrow(); }); it("does not throw when playBackgroundMusic and stopBackgroundMusic are called", () => { @@ -213,7 +212,7 @@ describe("SoundManager", () => { throw new Error("audio backend failure"); }); }); - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); const clickHowl = howlInstances[howlInstances.length - 1]; clickHowl.play.mockImplementation(() => { throw new Error("audio backend failure"); @@ -229,8 +228,8 @@ describe("SoundManager", () => { expect(() => soundManager.stopBackgroundMusic()).not.toThrow(); expect(() => soundManager.setBackgroundMusicVolume(0.5)).not.toThrow(); expect(() => soundManager.setSoundEffectsVolume(0.5)).not.toThrow(); - expect(() => soundManager.playSoundEffect(SoundEffect.Click)).not.toThrow(); - expect(() => soundManager.stopSoundEffect(SoundEffect.Click)).not.toThrow(); + expect(() => soundManager.playSoundEffect("click")).not.toThrow(); + expect(() => soundManager.stopSoundEffect("click")).not.toThrow(); }); }); @@ -247,28 +246,28 @@ describe("Sound channel management", () => { it("new sound always plays even when at channel cap", () => { for (let i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); } - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AtomHit)); + eventBus.emit(new PlaySoundEffectEvent("atom-hit")); const atomHowl = howlInstances[howlInstances.length - 1]; expect(atomHowl.play).toHaveBeenCalled(); }); it("stops the oldest sound when at channel cap", () => { for (let i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); } const clickHowl = howlInstances[howlInstances.length - 1]; // The first play had id=1. Playing one more should stop id=1. - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.AtomHit)); + eventBus.emit(new PlaySoundEffectEvent("atom-hit")); expect(clickHowl.stop).toHaveBeenCalledWith(1); }); it("frees a channel when a sound ends naturally", () => { for (let i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); } const clickHowl = howlInstances[howlInstances.length - 1]; @@ -277,13 +276,13 @@ describe("Sound channel management", () => { // Next sound should play without stopping anything clickHowl.stop.mockClear(); - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); expect(clickHowl.stop).not.toHaveBeenCalled(); }); it("allows up to MAX_CONCURRENT_SOUNDS without stopping any", () => { for (let i = 0; i < MAX_CONCURRENT_SOUNDS; i++) { - eventBus.emit(new PlaySoundEffectEvent(SoundEffect.Click)); + eventBus.emit(new PlaySoundEffectEvent("click")); } const clickHowl = howlInstances[howlInstances.length - 1]; expect(clickHowl.play).toHaveBeenCalledTimes(8);