Back to typed union

This commit is contained in:
VariableVince
2026-05-24 18:15:08 +02:00
parent 2f3abfdae3
commit 35064e0f3e
6 changed files with 73 additions and 97 deletions
+13 -27
View File
@@ -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"));
}
}
}
+2 -4
View File
@@ -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", {
+4 -4
View File
@@ -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(),
+2 -2
View File
@@ -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 &&
+33 -40
View File
@@ -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<SoundEffect, string> = 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 {
+19 -20
View File
@@ -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);