Merge pull request 'feat: add audio features and sounds to the game' (!197) from task/CAS-78/AddSoundEffects into main
All checks were successful
Release / Release (push) Successful in 58s
Release / Build Backend Image (push) Successful in 24s
Release / Build Frontend Image (push) Successful in 27s

Reviewed-on: #197
Reviewed-by: Constantin Simonis <constantin@simonis.lol>
This commit is contained in:
Jan-Marlon Leibl 2025-05-15 12:15:22 +00:00
commit 84feb5f080
No known key found for this signature in database
GPG key ID: 944223E4D46B7412
12 changed files with 140 additions and 1 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,10 +1,12 @@
import { Component, HostListener, signal } from '@angular/core'; import { Component, HostListener, inject, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from './shared/components/navbar/navbar.component'; import { NavbarComponent } from './shared/components/navbar/navbar.component';
import { FooterComponent } from './shared/components/footer/footer.component'; import { FooterComponent } from './shared/components/footer/footer.component';
import { LoginComponent } from './feature/auth/login/login.component'; import { LoginComponent } from './feature/auth/login/login.component';
import { RegisterComponent } from './feature/auth/register/register.component'; import { RegisterComponent } from './feature/auth/register/register.component';
import { RecoverPasswordComponent } from './feature/auth/recover-password/recover-password.component'; import { RecoverPasswordComponent } from './feature/auth/recover-password/recover-password.component';
import { PlaySoundDirective } from './shared/directives/play-sound.directive';
import { SoundInitializerService } from './shared/services/sound-initializer.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -16,14 +18,22 @@ import { RecoverPasswordComponent } from './feature/auth/recover-password/recove
LoginComponent, LoginComponent,
RegisterComponent, RegisterComponent,
RecoverPasswordComponent, RecoverPasswordComponent,
PlaySoundDirective,
], ],
templateUrl: './app.component.html', templateUrl: './app.component.html',
hostDirectives: [PlaySoundDirective],
}) })
export class AppComponent { export class AppComponent {
private soundInitializer = inject(SoundInitializerService);
showLogin = signal(false); showLogin = signal(false);
showRegister = signal(false); showRegister = signal(false);
showRecoverPassword = signal(false); showRecoverPassword = signal(false);
constructor() {
this.soundInitializer.initialize();
}
@HostListener('document:keydown.escape') @HostListener('document:keydown.escape')
handleEscapeKey() { handleEscapeKey() {
this.hideAuthForms(); this.hideAuthForms();

View file

@ -14,6 +14,7 @@ import { UserService } from '@service/user.service';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component'; import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
import { AuthService } from '@service/auth.service'; import { AuthService } from '@service/auth.service';
import { AudioService } from '@shared/services/audio.service';
@Component({ @Component({
selector: 'app-blackjack', selector: 'app-blackjack',
@ -35,6 +36,7 @@ export default class BlackjackComponent implements OnInit {
private userService = inject(UserService); private userService = inject(UserService);
private authService = inject(AuthService); private authService = inject(AuthService);
private blackjackService = inject(BlackjackService); private blackjackService = inject(BlackjackService);
private audioService = inject(AudioService);
dealerCards = signal<Card[]>([]); dealerCards = signal<Card[]>([]);
playerCards = signal<Card[]>([]); playerCards = signal<Card[]>([]);
@ -91,6 +93,9 @@ export default class BlackjackComponent implements OnInit {
// Show the result dialog after refreshing user data // Show the result dialog after refreshing user data
timer(500).subscribe(() => { timer(500).subscribe(() => {
this.showGameResult.set(true); this.showGameResult.set(true);
if (game.state === GameState.PLAYER_WON || game.state === GameState.PLAYER_BLACKJACK) {
this.audioService.playWinSound();
}
console.log('Game result dialog shown after delay'); console.log('Game result dialog shown after delay');
}); });
}); });
@ -99,6 +104,7 @@ export default class BlackjackComponent implements OnInit {
onNewGame(bet: number): void { onNewGame(bet: number): void {
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.startGame(bet).subscribe({ this.blackjackService.startGame(bet).subscribe({
next: (game) => { next: (game) => {
@ -117,6 +123,7 @@ export default class BlackjackComponent implements OnInit {
if (!this.currentGameId() || this.isActionInProgress()) return; if (!this.currentGameId() || this.isActionInProgress()) return;
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.hit(this.currentGameId()!).subscribe({ this.blackjackService.hit(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
@ -143,6 +150,7 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.stand(this.currentGameId()!).subscribe({ this.blackjackService.stand(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
@ -167,6 +175,7 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.audioService.playBetSound();
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {

View file

@ -40,6 +40,7 @@ export default class SlotsComponent implements OnInit, OnDestroy {
private userService = inject(UserService); private userService = inject(UserService);
private authService = inject(AuthService); private authService = inject(AuthService);
private userSubscription: Subscription | undefined; private userSubscription: Subscription | undefined;
private winSound: HTMLAudioElement;
slotInfo = signal<Record<string, number> | null>(null); slotInfo = signal<Record<string, number> | null>(null);
slotResult = signal<SlotResult>({ slotResult = signal<SlotResult>({
@ -56,6 +57,10 @@ export default class SlotsComponent implements OnInit, OnDestroy {
betAmount = signal<number>(1); betAmount = signal<number>(1);
isSpinning = false; isSpinning = false;
constructor() {
this.winSound = new Audio('/sounds/win.mp3');
}
ngOnInit(): void { ngOnInit(): void {
this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => { this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => {
this.slotInfo.set(data); this.slotInfo.set(data);
@ -111,6 +116,7 @@ export default class SlotsComponent implements OnInit, OnDestroy {
this.slotResult.set(result); this.slotResult.set(result);
if (result.status === 'win') { if (result.status === 'win') {
this.winSound.play();
this.userService.updateLocalBalance(result.amount); this.userService.updateLocalBalance(result.amount);
} }

View file

@ -24,6 +24,7 @@ export default class LootboxOpeningComponent {
prizeList: Reward[] = []; prizeList: Reward[] = [];
animationCompleted = false; animationCompleted = false;
currentUser: User | null = null; currentUser: User | null = null;
private winSound: HTMLAudioElement;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -33,6 +34,7 @@ export default class LootboxOpeningComponent {
private authService: AuthService, private authService: AuthService,
private cdr: ChangeDetectorRef private cdr: ChangeDetectorRef
) { ) {
this.winSound = new Audio('/sounds/win.mp3');
this.loadLootbox(); this.loadLootbox();
this.authService.userSubject.subscribe((user) => { this.authService.userSubject.subscribe((user) => {
this.currentUser = user; this.currentUser = user;
@ -145,6 +147,7 @@ export default class LootboxOpeningComponent {
this.animationCompleted = true; this.animationCompleted = true;
if (this.wonReward) { if (this.wonReward) {
this.winSound.play();
this.userService.updateLocalBalance(this.wonReward.value); this.userService.updateLocalBalance(this.wonReward.value);
} }

View file

@ -0,0 +1,15 @@
import { Directive, HostListener, inject } from '@angular/core';
import { AudioService } from '../services/audio.service';
@Directive({
selector: '[appPlaySound]',
standalone: true,
})
export class PlaySoundDirective {
private audioService = inject(AudioService);
@HostListener('click')
onClick() {
this.audioService.playBetSound();
}
}

View file

@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class AudioService {
private audioCache = new Map<string, HTMLAudioElement>();
private getAudio(soundName: string): HTMLAudioElement {
if (this.audioCache.has(soundName)) {
return this.audioCache.get(soundName)!;
}
const audio = new Audio(`/sounds/${soundName}.mp3`);
this.audioCache.set(soundName, audio);
return audio;
}
playBetSound(): void {
const audio = this.getAudio('bet.mp3');
audio.currentTime = 0;
audio.play().catch((error) => console.error('Error playing bet sound:', error));
}
playWinSound(): void {
const audio = this.getAudio('win.mp3');
audio.currentTime = 0;
audio.play().catch((error) => console.error('Error playing win sound:', error));
}
}

View file

@ -0,0 +1,51 @@
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class SoundInitializerService {
private renderer: Renderer2;
private observer: MutationObserver;
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLElement) {
this.processElement(node);
}
});
});
});
}
initialize() {
document.querySelectorAll('button, a').forEach((element) => {
if (!element.hasAttribute('appPlaySound')) {
this.renderer.setAttribute(element, 'appPlaySound', '');
}
});
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
}
private processElement(element: HTMLElement) {
if (
(element.tagName === 'BUTTON' || element.tagName === 'A') &&
!element.hasAttribute('appPlaySound')
) {
this.renderer.setAttribute(element, 'appPlaySound', '');
}
element.querySelectorAll('button, a').forEach((child) => {
if (!child.hasAttribute('appPlaySound')) {
this.renderer.setAttribute(child, 'appPlaySound', '');
}
});
}
}

View file

@ -174,3 +174,18 @@ a {
.modal-card .button-secondary { .modal-card .button-secondary {
@apply bg-deep-blue-light/50 hover:bg-deep-blue-light w-full py-2.5 my-2 border border-deep-blue-light/30 hover:border-deep-blue-light/50; @apply bg-deep-blue-light/50 hover:bg-deep-blue-light w-full py-2.5 my-2 border border-deep-blue-light/30 hover:border-deep-blue-light/50;
} }
button,
a {
-webkit-tap-highlight-color: transparent;
}
button[appPlaySound],
a[appPlaySound] {
cursor: pointer;
}
button:not([appPlaySound]),
a:not([appPlaySound]) {
--add-sound-directive: true;
}