diff --git a/frontend/public/sounds/bet.mp3 b/frontend/public/sounds/bet.mp3 new file mode 100644 index 0000000..b3b7ca3 Binary files /dev/null and b/frontend/public/sounds/bet.mp3 differ diff --git a/frontend/public/sounds/coinflip.mp3 b/frontend/public/sounds/coinflip.mp3 new file mode 100644 index 0000000..f8708ea Binary files /dev/null and b/frontend/public/sounds/coinflip.mp3 differ diff --git a/frontend/public/sounds/drag.mp3 b/frontend/public/sounds/drag.mp3 new file mode 100644 index 0000000..cc7a53d Binary files /dev/null and b/frontend/public/sounds/drag.mp3 differ diff --git a/frontend/public/sounds/win.mp3 b/frontend/public/sounds/win.mp3 new file mode 100644 index 0000000..09441ef Binary files /dev/null and b/frontend/public/sounds/win.mp3 differ diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index a889011..4e55f7e 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -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 { NavbarComponent } from './shared/components/navbar/navbar.component'; import { FooterComponent } from './shared/components/footer/footer.component'; import { LoginComponent } from './feature/auth/login/login.component'; import { RegisterComponent } from './feature/auth/register/register.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({ selector: 'app-root', @@ -16,14 +18,22 @@ import { RecoverPasswordComponent } from './feature/auth/recover-password/recove LoginComponent, RegisterComponent, RecoverPasswordComponent, + PlaySoundDirective, ], templateUrl: './app.component.html', + hostDirectives: [PlaySoundDirective], }) export class AppComponent { + private soundInitializer = inject(SoundInitializerService); + showLogin = signal(false); showRegister = signal(false); showRecoverPassword = signal(false); + constructor() { + this.soundInitializer.initialize(); + } + @HostListener('document:keydown.escape') handleEscapeKey() { this.hideAuthForms(); diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 63c6a3a..e4b19b5 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -14,6 +14,7 @@ import { UserService } from '@service/user.service'; import { timer } from 'rxjs'; import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component'; import { AuthService } from '@service/auth.service'; +import { AudioService } from '@shared/services/audio.service'; @Component({ selector: 'app-blackjack', @@ -35,6 +36,7 @@ export default class BlackjackComponent implements OnInit { private userService = inject(UserService); private authService = inject(AuthService); private blackjackService = inject(BlackjackService); + private audioService = inject(AudioService); dealerCards = signal([]); playerCards = signal([]); @@ -91,6 +93,9 @@ export default class BlackjackComponent implements OnInit { // Show the result dialog after refreshing user data timer(500).subscribe(() => { 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'); }); }); @@ -99,6 +104,7 @@ export default class BlackjackComponent implements OnInit { onNewGame(bet: number): void { this.isActionInProgress.set(true); + this.audioService.playBetSound(); this.blackjackService.startGame(bet).subscribe({ next: (game) => { @@ -117,6 +123,7 @@ export default class BlackjackComponent implements OnInit { if (!this.currentGameId() || this.isActionInProgress()) return; this.isActionInProgress.set(true); + this.audioService.playBetSound(); this.blackjackService.hit(this.currentGameId()!).subscribe({ next: (game) => { @@ -143,6 +150,7 @@ export default class BlackjackComponent implements OnInit { } this.isActionInProgress.set(true); + this.audioService.playBetSound(); this.blackjackService.stand(this.currentGameId()!).subscribe({ next: (game) => { @@ -167,6 +175,7 @@ export default class BlackjackComponent implements OnInit { } this.isActionInProgress.set(true); + this.audioService.playBetSound(); this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ next: (game) => { diff --git a/frontend/src/app/feature/game/slots/slots.component.ts b/frontend/src/app/feature/game/slots/slots.component.ts index ff5a086..b297875 100644 --- a/frontend/src/app/feature/game/slots/slots.component.ts +++ b/frontend/src/app/feature/game/slots/slots.component.ts @@ -40,6 +40,7 @@ export default class SlotsComponent implements OnInit, OnDestroy { private userService = inject(UserService); private authService = inject(AuthService); private userSubscription: Subscription | undefined; + private winSound: HTMLAudioElement; slotInfo = signal | null>(null); slotResult = signal({ @@ -56,6 +57,10 @@ export default class SlotsComponent implements OnInit, OnDestroy { betAmount = signal(1); isSpinning = false; + constructor() { + this.winSound = new Audio('/sounds/win.mp3'); + } + ngOnInit(): void { this.httpClient.get>('/backend/slots/info').subscribe((data) => { this.slotInfo.set(data); @@ -111,6 +116,7 @@ export default class SlotsComponent implements OnInit, OnDestroy { this.slotResult.set(result); if (result.status === 'win') { + this.winSound.play(); this.userService.updateLocalBalance(result.amount); } diff --git a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts index 20faa02..3faf5be 100644 --- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts +++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts @@ -24,6 +24,7 @@ export default class LootboxOpeningComponent { prizeList: Reward[] = []; animationCompleted = false; currentUser: User | null = null; + private winSound: HTMLAudioElement; constructor( private route: ActivatedRoute, @@ -33,6 +34,7 @@ export default class LootboxOpeningComponent { private authService: AuthService, private cdr: ChangeDetectorRef ) { + this.winSound = new Audio('/sounds/win.mp3'); this.loadLootbox(); this.authService.userSubject.subscribe((user) => { this.currentUser = user; @@ -145,6 +147,7 @@ export default class LootboxOpeningComponent { this.animationCompleted = true; if (this.wonReward) { + this.winSound.play(); this.userService.updateLocalBalance(this.wonReward.value); } diff --git a/frontend/src/app/shared/directives/play-sound.directive.ts b/frontend/src/app/shared/directives/play-sound.directive.ts new file mode 100644 index 0000000..f949f64 --- /dev/null +++ b/frontend/src/app/shared/directives/play-sound.directive.ts @@ -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(); + } +} diff --git a/frontend/src/app/shared/services/audio.service.ts b/frontend/src/app/shared/services/audio.service.ts new file mode 100644 index 0000000..db44378 --- /dev/null +++ b/frontend/src/app/shared/services/audio.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class AudioService { + private audioCache = new Map(); + + 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)); + } +} diff --git a/frontend/src/app/shared/services/sound-initializer.service.ts b/frontend/src/app/shared/services/sound-initializer.service.ts new file mode 100644 index 0000000..47d09e0 --- /dev/null +++ b/frontend/src/app/shared/services/sound-initializer.service.ts @@ -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', ''); + } + }); + } +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 300a61a..57a0f6a 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -174,3 +174,18 @@ a { .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; } + +button, +a { + -webkit-tap-highlight-color: transparent; +} + +button[appPlaySound], +a[appPlaySound] { + cursor: pointer; +} + +button:not([appPlaySound]), +a:not([appPlaySound]) { + --add-sound-directive: true; +}