From 5809757bc9d54d5f6a4c9feb03ff4306530b6668 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 15 May 2025 14:03:26 +0200 Subject: [PATCH 1/3] feat: add audio features and sounds to the game --- frontend/public/sounds/bet.mp3 | Bin 0 -> 2925 bytes frontend/public/sounds/coinflip.mp3 | Bin 0 -> 14115 bytes frontend/public/sounds/drag.mp3 | Bin 0 -> 2626 bytes frontend/public/sounds/win.mp3 | Bin 0 -> 15725 bytes frontend/src/app/app.component.ts | 12 ++++- .../game/blackjack/blackjack.component.ts | 9 ++++ .../app/feature/game/slots/slots.component.ts | 6 +++ .../lootbox-opening.component.ts | 3 ++ .../shared/directives/play-sound.directive.ts | 15 ++++++ .../src/app/shared/services/audio.service.ts | 29 +++++++++++ .../services/sound-initializer.service.ts | 48 ++++++++++++++++++ frontend/src/styles.css | 12 +++++ 12 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 frontend/public/sounds/bet.mp3 create mode 100644 frontend/public/sounds/coinflip.mp3 create mode 100644 frontend/public/sounds/drag.mp3 create mode 100644 frontend/public/sounds/win.mp3 create mode 100644 frontend/src/app/shared/directives/play-sound.directive.ts create mode 100644 frontend/src/app/shared/services/audio.service.ts create mode 100644 frontend/src/app/shared/services/sound-initializer.service.ts diff --git a/frontend/public/sounds/bet.mp3 b/frontend/public/sounds/bet.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..b3b7ca39854d3d6aa32bdc67f2acea8e50be0ddd GIT binary patch literal 2925 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 literal 0 HcmV?d00001 diff --git a/frontend/public/sounds/drag.mp3 b/frontend/public/sounds/drag.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..cc7a53d4156c9cfa6a11ba7b36b863d8fd0c1d85 GIT binary patch literal 2626 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⪼I_+#J%Wx(qF3Exzy2A?DHOp~MvBLi zY)8b3BwFGbcJWK(x^3s^GSpIscOSOZ*$+J*J$RXV4)5Do-fVW!wq9d=()&HsY7Nj@ z{gB`zYoCwd$%m+};>%0rn->h+dR*MbFEJULx&5|1c~O6U{=r!HF82x1+3sRVcv<&I zzPqb?sw+Jk-6dgEEO@Ja64^?f4eC-n0*M<{Ah zlH*A zZ3s<0nH#Yb6%5LFGdu-r=p1NsUs~Mg3lyD|JZva=Aso6Kz(O*+Q*E`I(-#FQ?o+JF z-WojlT+-9wK9IX!kAD$uVZD!(JCJzVCrmm2mX~rhUUEfnyL8{L%<`xMP3n^0<#sZnyE5pZk->mzU#Ec}g*)vJPgeg_AN|Gri{% zK!3jD4@9IGmEapn;{r%K-<8*->^8p?Hcr7mJd|x#l3%Kmdk`)a*Re>iH;S&AOPf;e zx)K>hX2dmF%PL?7WYNBGRWWG!WqC;GG*mjM(R>2YDeft9pA88$K+m$z)&@5aIdPr# z`p*U>!4Q|+qzt|Xa2JoHcj~$Smen<@U`4?k*?D&?Geoc`)9Ou3xs!A=*`8sGSWQSr zmM_1O1C9>0Ryjz;D_WUW5hI=A-tsVB2pQR$YMP{vz!4^uHi7!x{YVIt(kPzS7=RkE z58ix9`eU&B^nFE9XL7?CF`vXt{5=N{To+t9qr2AjSM% zx|Fr$?K?;tukDugz7(4}DXXm+QjYlaT{(X{j`%2h#9Y(6u>Vx0+w#(2otC1dnq!sx z*)L6Tj7ui8Np)S4KJ7Ra0RZ(YUHyzV;-Df9E3ffHyk-|A?AIN+nTy2J^>=zaRYb4K zF)xQ>3reEwgDIJ1uTy1}?Xu!#l?pnzy7jtaY}?wilAs2Zii4ekgpOtUIzzH`XEzi@ zW%t);?6jF$bOZ8fPMP!DY}UjcNMCB_QXQBh8f(09owUe2UGNKhe{dHWqsOnRk@bFZ$^ z_DntCX8#nYvYXzYI-jgVx~dS$Gs;Y75%A|e3W|rA^0#?IiR^PfN_Rw?E}$uxos8h8 z0cm&BipgwQwLV4C$*b?nv#YIgYV!$5`EYXhqkZgpDc@#Kj{U$>=$pBT;Xm33&}K9| z$?rT&16p9>Sso=pWI7_^?eJ|$6I8RM1d3hT(Z4dhJ+ncpi$`r3EWvkMma(Z<{oQbV zvKO1n;wDVMDq8tw@taVE%hltu^2ra6>|Sd?5VI#%>;`IEjAL$eo!rGPhD80S5q0nI z3%*4>87mT9nMGC`0SAbIE>(Gs#65oN6(@+FFi@!=O;+o56eGo>mwFqT7WR0(7YRNADo05sj)wa%-dn3PK#31(ImQ%Zzd z_Wim%!;ii5H2g;yFEIMxrUHDn=e}a_^WZ)-A2Ojrf!&evwFPU5UcF!s&wsR^qQm^m zL=beyJxs^sE3CjKinB7mE1v5=c2qNJ6~b-rTSE%tmZu-YN1qhj3Lky6u>%AF0GDN+ zy$njEBHqjSHCl!yHZkLMbXvitUcdAGng-G^9YVv9Dn<7c=joX?28mW1+kk!XxvAt0 zhLt7;jeTZdWmjzqv*NzA65e=>ROp$9T*9s%aK3-K$utsvNo%k3_JW%^AOdPR%DSH< zMy<;`oM97Q2cl%=MXXIT`DvKM+@V+ewz#at;YfqPg~DrH)(z5pM91jQ>}{l1LU&~H zt6fysYPiOzpJO64@CrQeR#UKXJ^)CN+s8yjWad!E{(g9LJcf!=qFs9Y-eK`#UWmzr z%%5JSN2VT0N~_Ez-_~elx6JP_`a*S(Gqr$5(JMfMXt(jY3C-NP+QZW3`A7N#a$!y8C=b zNeue)7K@M9HP^jOyTt([]); 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..1b2d3e1 --- /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(); + } +} \ No newline at end of file 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..8e9b4d9 --- /dev/null +++ b/frontend/src/app/shared/services/audio.service.ts @@ -0,0 +1,29 @@ +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)) { + const audio = new Audio(`/sounds/${soundName}`); + this.audioCache.set(soundName, audio); + return audio; + } + return this.audioCache.get(soundName)!; + } + + 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)); + } +} \ No newline at end of file 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..f60f27d --- /dev/null +++ b/frontend/src/app/shared/services/sound-initializer.service.ts @@ -0,0 +1,48 @@ +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', ''); + } + }); + } +} \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 300a61a..beb0723 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -174,3 +174,15 @@ 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; +} From 6d353cc20246954ba0d7dd2cbaf8517d56d93d71 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 15 May 2025 14:04:43 +0200 Subject: [PATCH 2/3] style: fix formatting and add missing commas in code --- frontend/src/app/app.component.ts | 4 ++-- .../shared/directives/play-sound.directive.ts | 4 ++-- .../src/app/shared/services/audio.service.ts | 8 ++++---- .../services/sound-initializer.service.ts | 17 ++++++++++------- frontend/src/styles.css | 9 ++++++--- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 90ab687..4e55f7e 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -21,11 +21,11 @@ import { SoundInitializerService } from './shared/services/sound-initializer.ser PlaySoundDirective, ], templateUrl: './app.component.html', - hostDirectives: [PlaySoundDirective] + hostDirectives: [PlaySoundDirective], }) export class AppComponent { private soundInitializer = inject(SoundInitializerService); - + showLogin = signal(false); showRegister = signal(false); showRecoverPassword = signal(false); diff --git a/frontend/src/app/shared/directives/play-sound.directive.ts b/frontend/src/app/shared/directives/play-sound.directive.ts index 1b2d3e1..f949f64 100644 --- a/frontend/src/app/shared/directives/play-sound.directive.ts +++ b/frontend/src/app/shared/directives/play-sound.directive.ts @@ -3,7 +3,7 @@ import { AudioService } from '../services/audio.service'; @Directive({ selector: '[appPlaySound]', - standalone: true + standalone: true, }) export class PlaySoundDirective { private audioService = inject(AudioService); @@ -12,4 +12,4 @@ export class PlaySoundDirective { onClick() { this.audioService.playBetSound(); } -} \ No newline at end of file +} diff --git a/frontend/src/app/shared/services/audio.service.ts b/frontend/src/app/shared/services/audio.service.ts index 8e9b4d9..5e716fb 100644 --- a/frontend/src/app/shared/services/audio.service.ts +++ b/frontend/src/app/shared/services/audio.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class AudioService { private audioCache = new Map(); @@ -18,12 +18,12 @@ export class AudioService { playBetSound(): void { const audio = this.getAudio('bet.mp3'); audio.currentTime = 0; - audio.play().catch(error => console.error('Error playing bet sound:', error)); + 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)); + audio.play().catch((error) => console.error('Error playing win sound:', error)); } -} \ No newline at end of file +} diff --git a/frontend/src/app/shared/services/sound-initializer.service.ts b/frontend/src/app/shared/services/sound-initializer.service.ts index f60f27d..47d09e0 100644 --- a/frontend/src/app/shared/services/sound-initializer.service.ts +++ b/frontend/src/app/shared/services/sound-initializer.service.ts @@ -1,7 +1,7 @@ import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SoundInitializerService { private renderer: Renderer2; @@ -9,7 +9,7 @@ export class SoundInitializerService { constructor(rendererFactory: RendererFactory2) { this.renderer = rendererFactory.createRenderer(null, null); - + this.observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { @@ -22,7 +22,7 @@ export class SoundInitializerService { } initialize() { - document.querySelectorAll('button, a').forEach(element => { + document.querySelectorAll('button, a').forEach((element) => { if (!element.hasAttribute('appPlaySound')) { this.renderer.setAttribute(element, 'appPlaySound', ''); } @@ -30,19 +30,22 @@ export class SoundInitializerService { this.observer.observe(document.body, { childList: true, - subtree: true + subtree: true, }); } private processElement(element: HTMLElement) { - if ((element.tagName === 'BUTTON' || element.tagName === 'A') && !element.hasAttribute('appPlaySound')) { + if ( + (element.tagName === 'BUTTON' || element.tagName === 'A') && + !element.hasAttribute('appPlaySound') + ) { this.renderer.setAttribute(element, 'appPlaySound', ''); } - element.querySelectorAll('button, a').forEach(child => { + element.querySelectorAll('button, a').forEach((child) => { if (!child.hasAttribute('appPlaySound')) { this.renderer.setAttribute(child, 'appPlaySound', ''); } }); } -} \ No newline at end of file +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index beb0723..57a0f6a 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -175,14 +175,17 @@ a { @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 { +button, +a { -webkit-tap-highlight-color: transparent; } -button[appPlaySound], a[appPlaySound] { +button[appPlaySound], +a[appPlaySound] { cursor: pointer; } -button:not([appPlaySound]), a:not([appPlaySound]) { +button:not([appPlaySound]), +a:not([appPlaySound]) { --add-sound-directive: true; } From 566ea569e1fcdebf9744af521e3e62c2bfcc1be0 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Thu, 15 May 2025 14:14:18 +0200 Subject: [PATCH 3/3] refactor(audio.service): improve audio caching logic --- frontend/src/app/shared/services/audio.service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/shared/services/audio.service.ts b/frontend/src/app/shared/services/audio.service.ts index 5e716fb..db44378 100644 --- a/frontend/src/app/shared/services/audio.service.ts +++ b/frontend/src/app/shared/services/audio.service.ts @@ -7,12 +7,13 @@ export class AudioService { private audioCache = new Map(); private getAudio(soundName: string): HTMLAudioElement { - if (!this.audioCache.has(soundName)) { - const audio = new Audio(`/sounds/${soundName}`); - this.audioCache.set(soundName, audio); - return audio; + if (this.audioCache.has(soundName)) { + return this.audioCache.get(soundName)!; } - return this.audioCache.get(soundName)!; + + const audio = new Audio(`/sounds/${soundName}.mp3`); + this.audioCache.set(soundName, audio); + return audio; } playBetSound(): void {