Implement a "ka-ching" sound effect on kill (#2097)

## Description:
Building off of [this
PR](https://github.com/openfrontio/OpenFrontIO/pull/2090) which
implemented music, I extend this functionality to add sound effects.
Diff will be reduced if and when that PR gets merged!

I think the game would benefit from more sound effects, and adding a
"ka-ching" sound effect on kill seems like an easy place to start.

The ka-ching sound effect was found
[here](https://freesound.org/people/AKkingStudio/sounds/684165/) and is
licensed under Creative Commons.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

Demo video with sound:

https://github.com/user-attachments/assets/18c857a4-a741-492a-bbc1-68d4f3ba38da


## Please put your Discord username so you can be contacted if a bug or
regression is found:
basedgob

---------

Co-authored-by: icslucas <carolinacarazolli@gmail.com>
This commit is contained in:
Gabe Kauffman
2025-10-02 19:27:06 -04:00
committed by GitHub
parent ed062c9dbc
commit 5b36c02ff0
6 changed files with 115 additions and 7 deletions
+2 -1
View File
@@ -380,7 +380,8 @@
"terrain_disabled": "Terrain view disabled",
"exit_game_label": "Exit Game",
"exit_game_info": "Return to main menu",
"background_music_volume": "Background Music Volume"
"background_music_volume": "Background Music Volume",
"sound_effects_volume": "Sound Effects Volume"
},
"chat": {
"title": "Quick Chat",
Binary file not shown.
+3
View File
@@ -7,6 +7,7 @@ import {
RailroadUpdate,
} from "../../../core/game/GameUpdates";
import { GameView, UnitView } from "../../../core/game/GameView";
import SoundManager, { SoundEffect } from "../../sound/SoundManager";
import { renderNumber } from "../../Utils";
import { AnimatedSpriteLoader } from "../AnimatedSpriteLoader";
import { conquestFxFactory } from "../fx/ConquestFx";
@@ -216,6 +217,8 @@ export class FxLayer implements Layer {
return;
}
SoundManager.playSoundEffect(SoundEffect.KaChing);
const conquestFx = conquestFxFactory(
this.animatedSpriteLoader,
conquest,
@@ -50,6 +50,7 @@ export class SettingsModal extends LitElement implements Layer {
SoundManager.setBackgroundMusicVolume(
this.userSettings.backgroundMusicVolume(),
);
SoundManager.setSoundEffectsVolume(this.userSettings.soundEffectsVolume());
this.eventBus.on(ShowSettingsModalEvent, (event) => {
this.isVisible = event.isVisible;
this.shouldPause = event.shouldPause;
@@ -162,6 +163,13 @@ export class SettingsModal extends LitElement implements Layer {
this.requestUpdate();
}
private onSoundEffectsVolumeChange(event: Event) {
const volume = parseFloat((event.target as HTMLInputElement).value) / 100;
this.userSettings.setSoundEffectsVolume(volume);
SoundManager.setSoundEffectsVolume(volume);
this.requestUpdate();
}
render() {
if (!this.isVisible) {
return null;
@@ -221,6 +229,33 @@ export class SettingsModal extends LitElement implements Layer {
</div>
</div>
<div
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
>
<img
src=${musicIcon}
alt="soundEffectsIcon"
width="20"
height="20"
/>
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.sound_effects_volume")}
</div>
<input
type="range"
min="0"
max="100"
.value=${this.userSettings.soundEffectsVolume() * 100}
@input=${this.onSoundEffectsVolumeChange}
class="w-full border border-slate-500 rounded-lg"
/>
</div>
<div class="text-sm text-slate-400">
${Math.round(this.userSettings.soundEffectsVolume() * 100)}%
</div>
</div>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onTerrainButtonClick}"
+67 -6
View File
@@ -1,22 +1,42 @@
import { Howl, Howler } from "howler";
import { Howl } from "howler";
import of4 from "../../../proprietary/sounds/music/of4.mp3";
import openfront from "../../../proprietary/sounds/music/openfront.mp3";
import war from "../../../proprietary/sounds/music/war.mp3";
import kaChingSound from "../../../resources/sounds/effects/ka-ching.mp3";
export enum SoundEffect {
KaChing = "ka-ching",
}
class SoundManager {
private backgroundMusic: Howl[] = [];
private currentTrack: number = 0;
private soundEffects: Map<SoundEffect, Howl> = new Map();
private soundEffectsVolume: number = 1;
private backgroundMusicVolume: number = 0;
constructor() {
this.backgroundMusic = [
new Howl({ src: [of4], loop: false, onend: this.playNext.bind(this) }),
new Howl({
src: [of4],
loop: false,
onend: this.playNext.bind(this),
volume: 0,
}),
new Howl({
src: [openfront],
loop: false,
onend: this.playNext.bind(this),
volume: 0,
}),
new Howl({
src: [war],
loop: false,
onend: this.playNext.bind(this),
volume: 0,
}),
new Howl({ src: [war], loop: false, onend: this.playNext.bind(this) }),
];
this.setBackgroundMusicVolume(0);
this.loadSoundEffect(SoundEffect.KaChing, kaChingSound);
}
public playBackgroundMusic(): void {
@@ -35,14 +55,55 @@ class SoundManager {
}
public setBackgroundMusicVolume(volume: number): void {
const newVolume = Math.max(0, Math.min(1, volume));
Howler.volume(newVolume);
this.backgroundMusicVolume = Math.max(0, Math.min(1, volume));
this.backgroundMusic.forEach((track) => {
track.volume(this.backgroundMusicVolume);
});
}
private playNext(): void {
this.currentTrack = (this.currentTrack + 1) % this.backgroundMusic.length;
this.playBackgroundMusic();
}
public loadSoundEffect(name: SoundEffect, src: string): void {
if (!this.soundEffects.has(name)) {
const sound = new Howl({
src: [src],
volume: this.soundEffectsVolume,
});
this.soundEffects.set(name, sound);
}
}
public playSoundEffect(name: SoundEffect): void {
const sound = this.soundEffects.get(name);
if (sound) {
sound.play();
}
}
public setSoundEffectsVolume(volume: number): void {
this.soundEffectsVolume = Math.max(0, Math.min(1, volume));
this.soundEffects.forEach((sound) => {
sound.volume(this.soundEffectsVolume);
});
}
public stopSoundEffect(name: SoundEffect): void {
const sound = this.soundEffects.get(name);
if (sound) {
sound.stop();
}
}
public unloadSoundEffect(name: SoundEffect): void {
const sound = this.soundEffects.get(name);
if (sound) {
sound.unload();
this.soundEffects.delete(name);
}
}
}
export default new SoundManager();
+8
View File
@@ -176,4 +176,12 @@ export class UserSettings {
setBackgroundMusicVolume(volume: number): void {
this.setFloat("settings.backgroundMusicVolume", volume);
}
soundEffectsVolume(): number {
return this.getFloat("settings.soundEffectsVolume", 1);
}
setSoundEffectsVolume(volume: number): void {
this.setFloat("settings.soundEffectsVolume", volume);
}
}