## Description:
add music to the game
Describe the PR.
add music
<img width="549" height="770" alt="image"
src="https://github.com/user-attachments/assets/d8457d85-6f63-4024-8b69-572f8c9bb225"
/>

## 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

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

Lucas
This commit is contained in:
icslucas
2025-09-30 20:13:32 +02:00
committed by GitHub
parent d48801d3d7
commit b31200a3ac
16 changed files with 141 additions and 3 deletions
+15 -2
View File
@@ -25,6 +25,7 @@
"express": "^4.21.1",
"express-rate-limit": "^7.5.0",
"fastpriorityqueue": "^0.7.5",
"howler": "^2.2.4",
"intl-messageformat": "^10.7.16",
"ip-anonymize": "^0.1.0",
"jose": "^6.0.10",
@@ -52,6 +53,7 @@
"@types/express": "^4.17.23",
"@types/google-protobuf": "^3.15.12",
"@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12",
"@types/jest": "^30.0.0",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
@@ -7131,6 +7133,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/howler": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.12.tgz",
"integrity": "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -9509,7 +9518,6 @@
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@@ -12292,6 +12300,12 @@
"he": "bin/he"
}
},
"node_modules/howler": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
"integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==",
"license": "MIT"
},
"node_modules/hpack.js": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
@@ -16221,7 +16235,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
+2
View File
@@ -39,6 +39,7 @@
"@types/express": "^4.17.23",
"@types/google-protobuf": "^3.15.12",
"@types/hammerjs": "^2.0.46",
"@types/howler": "^2.2.12",
"@types/jest": "^30.0.0",
"@types/jquery": "^3.5.31",
"@types/js-yaml": "^4.0.9",
@@ -119,6 +120,7 @@
"express": "^4.21.1",
"express-rate-limit": "^7.5.0",
"fastpriorityqueue": "^0.7.5",
"howler": "^2.2.4",
"intl-messageformat": "^10.7.16",
"ip-anonymize": "^0.1.0",
"jose": "^6.0.10",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 122.88 91.69" style="enable-background:new 0 0 122.88 91.69" xml:space="preserve"><style type="text/css"><![CDATA[
.st0{fill-rule:evenodd;clip-rule:evenodd;}
]]></style><g><path class="st0" d="M49.58,0h11.34v3.79c28.37,6.5,31.61,20.12,15.16,41.59c1.75-21.3-0.4-25.87-15.16-26.86v53.86 c0.03,0.29,0.04,0.57,0.04,0.86c0,7-7.35,13.94-16.4,15.51c-9.06,1.57-16.4-2.84-16.4-9.84c0-9.55,13.12-17.84,21.43-14.92L49.58,0 L49.58,0L49.58,0z M72.31,91.69h-0.16c0-1.29-0.48-2.41-1.44-3.38c-0.96-0.96-2.09-1.44-3.38-1.44v-0.16 c1.29,0,2.41-0.48,3.38-1.44c0.96-0.97,1.44-2.09,1.44-3.37h0.16c0,1.29,0.48,2.41,1.44,3.37c0.96,0.96,2.09,1.44,3.38,1.44v0.16 c-1.29,0-2.41,0.48-3.38,1.44C72.79,89.27,72.31,90.4,72.31,91.69L72.31,91.69L72.31,91.69z M92.53,74.61l-0.4-0.03 c0.24-3.22-0.75-6.13-2.97-8.72s-4.94-4.01-8.17-4.25l0.03-0.4c3.22,0.25,6.13-0.75,8.72-2.97c2.59-2.23,4.01-4.95,4.25-8.16 l0.4,0.03c-0.24,3.22,0.75,6.13,2.97,8.72s4.94,4.01,8.17,4.25l-0.03,0.4c-3.22-0.25-6.13,0.75-8.72,2.97 C94.19,68.67,92.78,71.39,92.53,74.61L92.53,74.61L92.53,74.61z M109.33,42.87l-0.5,0.06c-0.52-4.13-2.51-7.55-5.99-10.26 c-3.47-2.71-7.28-3.8-11.42-3.29l-0.06-0.5c4.13-0.51,7.55-2.51,10.26-5.99s3.8-7.29,3.29-11.41l0.5-0.06 c0.52,4.13,2.51,7.55,5.99,10.26c3.48,2.71,7.28,3.8,11.42,3.29l0.06,0.5c-4.13,0.52-7.55,2.51-10.26,5.99 C109.91,34.93,108.82,38.74,109.33,42.87L109.33,42.87L109.33,42.87z M18.45,54.03c-2.3,0-4.17-1.87-4.17-4.17s1.87-4.17,4.17-4.17 h16.63c2.3,0,4.17,1.87,4.17,4.17s-1.87,4.17-4.17,4.17H18.45L18.45,54.03L18.45,54.03z M10.57,33.79c-2.3,0-4.17-1.87-4.17-4.17 c0-2.3,1.87-4.17,4.17-4.17h24.52c2.3,0,4.17,1.87,4.17,4.17c0,2.3-1.87,4.17-4.17,4.17H3.67L10.57,33.79L10.57,33.79z M4.17,13.55 C1.87,13.55,0,11.68,0,9.37c0-2.3,1.87-4.17,4.17-4.17h30.92c2.3,0,4.17,1.87,4.17,4.17c0,2.3-1.87,4.17-4.17,4.17L4.17,13.55 L4.17,13.55L4.17,13.55z" fill="#ffffff"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+2 -1
View File
@@ -360,7 +360,8 @@
"terrain_enabled": "Terrain view enabled",
"terrain_disabled": "Terrain view disabled",
"exit_game_label": "Exit Game",
"exit_game_info": "Return to main menu"
"exit_game_info": "Return to main menu",
"background_music_volume": "Background Music Volume"
},
"chat": {
"title": "Quick Chat",
+3
View File
@@ -47,6 +47,7 @@ import {
} from "./Transport";
import { createCanvas } from "./Utils";
import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import SoundManager from "./sound/SoundManager";
export interface LobbyConfig {
serverConfig: ServerConfig;
@@ -245,6 +246,7 @@ export class ClientGameRunner {
}
public start() {
SoundManager.playBackgroundMusic();
console.log("starting client game");
this.isActive = true;
@@ -372,6 +374,7 @@ export class ClientGameRunner {
}
public stop() {
SoundManager.stopBackgroundMusic();
if (!this.isActive) return;
this.isActive = false;
@@ -9,11 +9,13 @@ import mouseIcon from "../../../../resources/images/MouseIconWhite.svg";
import ninjaIcon from "../../../../resources/images/NinjaIconWhite.svg";
import settingsIcon from "../../../../resources/images/SettingIconWhite.svg";
import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
import musicIcon from "../../../../resources/images/music.svg";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
import { PauseGameEvent } from "../../Transport";
import { translateText } from "../../Utils";
import SoundManager from "../../sound/SoundManager";
import { Layer } from "./Layer";
export class ShowSettingsModalEvent {
@@ -45,6 +47,9 @@ export class SettingsModal extends LitElement implements Layer {
wasPausedWhenOpened = false;
init() {
SoundManager.setBackgroundMusicVolume(
this.userSettings.backgroundMusicVolume(),
);
this.eventBus.on(ShowSettingsModalEvent, (event) => {
this.isVisible = event.isVisible;
this.shouldPause = event.shouldPause;
@@ -150,6 +155,13 @@ export class SettingsModal extends LitElement implements Layer {
window.location.href = "/";
}
private onVolumeChange(event: Event) {
const volume = parseFloat((event.target as HTMLInputElement).value) / 100;
this.userSettings.setBackgroundMusicVolume(volume);
SoundManager.setBackgroundMusicVolume(volume);
this.requestUpdate();
}
render() {
if (!this.isVisible) {
return null;
@@ -187,6 +199,28 @@ export class SettingsModal extends LitElement implements Layer {
</div>
<div class="p-4 space-y-3">
<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="musicIcon" width="20" height="20" />
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.background_music_volume")}
</div>
<input
type="range"
min="0"
max="100"
.value=${this.userSettings.backgroundMusicVolume() * 100}
@input=${this.onVolumeChange}
class="w-full border border-slate-500 rounded-lg"
/>
</div>
<div class="text-sm text-slate-400">
${Math.round(this.userSettings.backgroundMusicVolume() * 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}"
+48
View File
@@ -0,0 +1,48 @@
import { Howl, Howler } 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";
class SoundManager {
private backgroundMusic: Howl[] = [];
private currentTrack: number = 0;
constructor() {
this.backgroundMusic = [
new Howl({ src: [of4], loop: false, onend: this.playNext.bind(this) }),
new Howl({
src: [openfront],
loop: false,
onend: this.playNext.bind(this),
}),
new Howl({ src: [war], loop: false, onend: this.playNext.bind(this) }),
];
this.setBackgroundMusicVolume(0);
}
public playBackgroundMusic(): void {
if (
this.backgroundMusic.length > 0 &&
!this.backgroundMusic[this.currentTrack].playing()
) {
this.backgroundMusic[this.currentTrack].play();
}
}
public stopBackgroundMusic(): void {
if (this.backgroundMusic.length > 0) {
this.backgroundMusic[this.currentTrack].stop();
}
}
public setBackgroundMusicVolume(volume: number): void {
const newVolume = Math.max(0, Math.min(1, volume));
Howler.volume(newVolume);
}
private playNext(): void {
this.currentTrack = (this.currentTrack + 1) % this.backgroundMusic.length;
this.playBackgroundMusic();
}
}
export default new SoundManager();
+22
View File
@@ -19,6 +19,20 @@ export class UserSettings {
localStorage.setItem(key, value ? "true" : "false");
}
getFloat(key: string, defaultValue: number): number {
const value = localStorage.getItem(key);
if (!value) return defaultValue;
const floatValue = parseFloat(value);
if (isNaN(floatValue)) return defaultValue;
return floatValue;
}
setFloat(key: string, value: number) {
localStorage.setItem(key, value.toString());
}
emojis() {
return this.get("settings.emojis", true);
}
@@ -154,4 +168,12 @@ export class UserSettings {
localStorage.setItem(PATTERN_KEY, patternName);
}
}
backgroundMusicVolume(): number {
return this.getFloat("settings.backgroundMusicVolume", 0);
}
setBackgroundMusicVolume(volume: number): void {
this.setFloat("settings.backgroundMusicVolume", volume);
}
}
+5
View File
@@ -40,3 +40,8 @@ declare module "*.xml" {
const value: string;
export default value;
}
declare module "*.mp3" {
const value: string;
export default value;
}
+7
View File
@@ -93,6 +93,13 @@ export default async (env, argv) => {
filename: "fonts/[name].[contenthash][ext]", // Added content hash and fixed path
},
},
{
test: /\.(mp3|wav|ogg)$/i,
type: "asset/resource",
generator: {
filename: "sounds/[name].[contenthash][ext]",
},
},
],
},
resolve: {