From dce5d1a86ee75ff230619d251049601322210e1b Mon Sep 17 00:00:00 2001 From: Constantin Simonis Date: Wed, 21 May 2025 13:10:40 +0200 Subject: [PATCH 001/124] feat(email): add mail protocol configuration option --- .../java/de/szut/casino/security/service/EmailService.java | 1 + .../main/java/de/szut/casino/security/service/MailConfig.java | 3 +++ backend/src/main/resources/application.properties | 1 + 3 files changed, 5 insertions(+) diff --git a/backend/src/main/java/de/szut/casino/security/service/EmailService.java b/backend/src/main/java/de/szut/casino/security/service/EmailService.java index f276a3c..83d65e0 100644 --- a/backend/src/main/java/de/szut/casino/security/service/EmailService.java +++ b/backend/src/main/java/de/szut/casino/security/service/EmailService.java @@ -28,6 +28,7 @@ public class EmailService { this.mailConfig = mailConfig; this.mailSender.setHost(mailConfig.host); this.mailSender.setPort(mailConfig.port); + this.mailSender.setProtocol(mailConfig.protocol); if (mailConfig.authenticationEnabled) { this.mailSender.setUsername(mailConfig.username); this.mailSender.setPassword(mailConfig.password); diff --git a/backend/src/main/java/de/szut/casino/security/service/MailConfig.java b/backend/src/main/java/de/szut/casino/security/service/MailConfig.java index 56c7250..8a516fd 100644 --- a/backend/src/main/java/de/szut/casino/security/service/MailConfig.java +++ b/backend/src/main/java/de/szut/casino/security/service/MailConfig.java @@ -22,4 +22,7 @@ public class MailConfig { @Value("${app.mail.from-address}") public String fromAddress; + + @Value("${app.mail.protocol}") + public String protocol; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 0fa2407..7b761a7 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -14,6 +14,7 @@ app.mail.port=${MAIL_PORT:1025} app.mail.username=${MAIL_USER:null} app.mail.password=${MAIL_PASS:null} app.mail.from-address=${MAIL_FROM:casino@localhost} +app.mail.protocol=${MAIL_PROTOCOL:smtp} spring.application.name=casino From 7762048ee125cd2ae1ee0242a5a0b127a17dd421 Mon Sep 17 00:00:00 2001 From: Jan K9f Date: Wed, 21 May 2025 13:30:51 +0200 Subject: [PATCH 002/124] fix: Change lang to german --- .../game/coinflip/coinflip.component.html | 34 ++++---- .../game/coinflip/coinflip.component.ts | 82 ++++++++++--------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.html b/frontend/src/app/feature/game/coinflip/coinflip.component.html index 671bc31..8dd6be8 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.html +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.html @@ -5,13 +5,11 @@ @if (gameResult()) {

- {{ gameResult()?.isWin ? 'You Won!' : 'You Lost' }} + {{ gameResult()?.isWin ? 'Du hast gewonnen!' : 'Du hast verloren' }}

- Coin landed on: - {{ - gameResult()?.coinSide === 'HEAD' ? 'HEAD' : 'TAILS' - }} + Münze zeigt: + {{ gameResult()?.coinSide === 'HEAD' ? 'KOPF' : 'ZAHL' }}

@if (gameResult()?.isWin) {

@@ -35,7 +33,7 @@

-
HEAD
+
KOPF
@@ -43,7 +41,7 @@ class="back coin-side bg-gray-700 flex items-center justify-center text-2xl font-bold text-white" > - TAILS + ZAHL
@@ -56,7 +54,7 @@ class="button-primary py-3 px-6 relative text-lg" [class.opacity-50]="gameInProgress()" > - Bet TAILS + Auf ZAHL setzen @@ -72,11 +70,11 @@
-

Game Information

+

Spielinformationen

- Current Bet: + Aktueller Einsatz: @@ -84,7 +82,7 @@
- Your Balance: + Dein Guthaben: {{ balance() | currency: 'EUR' }} @@ -103,9 +101,9 @@
- + Cannot exceed balanceDarf Guthaben nicht überschreiten
-

How to Play

+

Spielregeln

    -
  • • Choose your bet amount
  • -
  • • Select Heads or Tails
  • -
  • • Win double your bet if correct
  • +
  • • Wähle deinen Einsatzbetrag
  • +
  • • Wähle Kopf oder Zahl
  • +
  • • Gewinne das Doppelte deines Einsatzes bei richtiger Wahl
diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.ts b/frontend/src/app/feature/game/coinflip/coinflip.component.ts index 1766e4d..112a300 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.ts +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.ts @@ -44,14 +44,14 @@ export default class CoinflipComponent implements OnInit { private coinflipSound?: HTMLAudioElement; ngOnInit(): void { - // Subscribe to user updates for real-time balance changes + // Abonniere Benutzerupdates für Echtzeitaktualisierungen des Guthabens this.authService.userSubject.subscribe((user) => { if (user) { this.balance.set(user.balance); } }); - // Initialize coinflip sound + // Initialisiere Münzwurf-Sound this.coinflipSound = new Audio('/sounds/coinflip.mp3'); } @@ -65,26 +65,26 @@ export default class CoinflipComponent implements OnInit { const inputElement = event.target as HTMLInputElement; let value = Number(inputElement.value); - // Reset invalid bet state + // Setze ungültigen Einsatz-Status zurück this.isInvalidBet.set(false); - // Enforce minimum bet of 1 + // Erzwinge Mindesteinsatz von 1 if (value <= 0) { value = 1; } - // Cap bet at available balance and show feedback + // Begrenze Einsatz auf verfügbares Guthaben und zeige Feedback if (value > this.balance()) { value = this.balance(); - // Show visual feedback + // Visuelles Feedback anzeigen this.isInvalidBet.set(true); - // Indicate the error briefly + // Zeige den Fehler kurz an setTimeout(() => this.isInvalidBet.set(false), 800); - // Update the input field directly to show the user the max value + // Aktualisiere das Eingabefeld direkt, um dem Benutzer den maximalen Wert anzuzeigen inputElement.value = String(value); } - // Update signals + // Aktualisiere Signale this.betInputValue.set(value); this.currentBet.set(value); } @@ -100,34 +100,34 @@ export default class CoinflipComponent implements OnInit { private placeBet(side: 'HEAD' | 'TAILS') { if (this.gameInProgress() || this.isActionInProgress()) return; - // Reset previous result + // Setze vorheriges Ergebnis zurück this.gameResult.set(null); this.errorMessage.set(''); - // Set game state + // Setze Spielstatus this.gameInProgress.set(true); this.isActionInProgress.set(true); - // Play bet sound + // Spiele Einsatz-Sound this.audioService.playBetSound(); - // Create bet request + // Erstelle Einsatz-Anfrage const request: CoinflipRequest = { betAmount: this.currentBet(), coinSide: side, }; - // Call API + // API aufrufen this.http .post('/backend/coinflip', request) .pipe( catchError((error) => { - console.error('Error playing coinflip:', error); + console.error('Fehler beim Spielen von Coinflip:', error); if (error.status === 400 && error.error.message.includes('insufficient')) { - this.errorMessage.set('Insufficient funds'); + this.errorMessage.set('Unzureichendes Guthaben'); } else { - this.errorMessage.set('An error occurred. Please try again.'); + this.errorMessage.set('Ein Fehler ist aufgetreten. Bitte versuche es erneut.'); } this.gameInProgress.set(false); @@ -140,37 +140,37 @@ export default class CoinflipComponent implements OnInit { .subscribe((result) => { if (!result) return; - console.log('API response:', result); + console.log('API-Antwort:', result); - // Fix potential property naming inconsistency from the backend + // Behebe mögliche Inkonsistenzen bei der Eigenschaftenbenennung vom Backend const fixedResult: CoinflipGame = { isWin: result.isWin ?? result.win, payout: result.payout, coinSide: result.coinSide, }; - console.log('Fixed result:', fixedResult); + console.log('Korrigiertes Ergebnis:', fixedResult); - // Play coin flip animation and sound + // Spiele Münzwurf-Animation und -Sound this.playCoinFlipAnimation(fixedResult.coinSide); - // Set result after animation completes + // Setze Ergebnis nach Abschluss der Animation setTimeout(() => { this.gameResult.set(fixedResult); - // Update balance with new value from auth service + // Aktualisiere Guthaben mit neuem Wert vom Auth-Service this.authService.loadCurrentUser(); - // Play win sound if player won + // Spiele Gewinn-Sound, wenn der Spieler gewonnen hat if (fixedResult.isWin) { this.audioService.playWinSound(); } - // Reset game state after showing result + // Setze Spielstatus nach Anzeigen des Ergebnisses zurück setTimeout(() => { this.gameInProgress.set(false); }, 1500); - }, 1100); // Just after animation ends + }, 1100); // Kurz nach Ende der Animation }); } @@ -179,48 +179,50 @@ export default class CoinflipComponent implements OnInit { const coinEl = this.coinElement.nativeElement; - // Reset any existing animations + // Setze bestehende Animationen zurück coinEl.classList.remove('animate-to-heads', 'animate-to-tails'); - // Reset any inline styles from previous animations + // Setze alle Inline-Styles von vorherigen Animationen zurück coinEl.style.transform = ''; - // Force a reflow to restart animation + // Erzwinge Reflow, um Animation neu zu starten void coinEl.offsetWidth; - // Play flip sound + // Spiele Münzwurf-Sound if (this.coinflipSound) { this.coinflipSound.currentTime = 0; - this.coinflipSound.play().catch((err) => console.error('Error playing sound:', err)); + this.coinflipSound + .play() + .catch((err) => console.error('Fehler beim Abspielen des Sounds:', err)); } - // Add appropriate animation class based on result + // Füge passende Animationsklasse basierend auf dem Ergebnis hinzu if (result === 'HEAD') { coinEl.classList.add('animate-to-heads'); } else { coinEl.classList.add('animate-to-tails'); } - console.log(`Animation applied for result: ${result}`); + console.log(`Animation angewendet für Ergebnis: ${result}`); } /** - * Validates input as the user types to prevent invalid values + * Validiert Eingabe während der Benutzer tippt, um ungültige Werte zu verhindern */ validateBetInput(event: KeyboardEvent) { - // Allow navigation keys (arrows, delete, backspace, tab) + // Erlaube Navigationstasten (Pfeile, Entf, Rücktaste, Tab) const navigationKeys = ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace', 'Tab']; if (navigationKeys.includes(event.key)) { return; } - // Only allow numbers + // Erlaube nur Zahlen if (!/^\d$/.test(event.key)) { event.preventDefault(); return; } - // Get the value that would result after the keypress + // Ermittle den Wert, der nach dem Tastendruck entstehen würde const input = event.target as HTMLInputElement; const currentValue = input.value; const cursorPosition = input.selectionStart || 0; @@ -230,14 +232,14 @@ export default class CoinflipComponent implements OnInit { currentValue.substring(input.selectionEnd || cursorPosition); const numValue = Number(newValue); - // Prevent values greater than balance + // Verhindere Werte, die größer als das Guthaben sind if (numValue > this.balance()) { event.preventDefault(); } } - // We removed the paste handler for simplicity since the updateBet method - // will handle any value that gets into the input field + // Der Paste-Handler wurde der Einfachheit halber entfernt, da die updateBet-Methode + // jeden Wert behandelt, der in das Eingabefeld gelangt getResultClass() { if (!this.gameResult()) return ''; From 1849500d744879f08d5ece0fec7ca53a6db1be95 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 21 May 2025 13:52:16 +0200 Subject: [PATCH 003/124] feat(dice): enhance game UI and add sound effects --- .../app/feature/game/dice/dice.component.html | 388 +++++++++++++----- .../app/feature/game/dice/dice.component.ts | 44 +- .../shared/directives/drag-sound.directive.ts | 39 ++ .../src/app/shared/services/audio.service.ts | 11 + 4 files changed, 374 insertions(+), 108 deletions(-) create mode 100644 frontend/src/app/shared/directives/drag-sound.directive.ts diff --git a/frontend/src/app/feature/game/dice/dice.component.html b/frontend/src/app/feature/game/dice/dice.component.html index 5cc7a0e..8d1ae51 100644 --- a/frontend/src/app/feature/game/dice/dice.component.html +++ b/frontend/src/app/feature/game/dice/dice.component.html @@ -1,121 +1,305 @@ -
-

Dice Game

+
+

Dice

+
+
+
+
+
+
+

+ Zielwert: + {{ + diceForm.get('targetValue')?.value | number: '1.0-2' + }} +

+
-
-
- -
-
- +
+
+ 0 + 25 + 50 + 75 + 100 +
+ + + +
+
+ + + +
+
+ {{ potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2' }} +
+ + + {{ diceForm.get('rollOver')?.value ? '>' : '<' }} + +
+ + + + @if (rolledValue() !== null) { +
+
+
+ } +
+ +
+ @for (i of [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; track i) { +
+ } +
+
+ + @if ( + hasError('targetValue', 'required') || + hasError('targetValue', 'min') || + hasError('targetValue', 'max') + ) { +
+ @if (hasError('targetValue', 'required')) { + Zielwert ist erforderlich + } + @if (hasError('targetValue', 'min')) { + Zielwert muss mindestens 1 sein + } + @if (hasError('targetValue', 'max')) { + Zielwert darf höchstens 99 sein + } +
+ } +
+ +
+ + +
+
+ + + @if (rolledValue() !== null) { +
+
+ @if (win()) { + + + +

+ Du hast gewonnen! Auszahlung: {{ payout() | currency: 'EUR' : 'symbol' : '1.2-2' }} +

+ } @else { + + + +

Du hast verloren.

+ } +
+
+ } +
+ +
+
+

Spielinformationen

+
+
+ Möglicher Gewinn: + {{ + potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2' + }} +
+ +
+ Gewinnchance: + {{ winChance() | number: '1.0-2' }}% +
+ +
+ + + + +
+ +
+
+ +
@if (hasError('betAmount', 'required')) { - Bet Amount is required + Einsatz ist erforderlich } @if (hasError('betAmount', 'min')) { - Bet Amount must be at least 0.01Einsatz muss mindestens 0.01 sein }
-
-
Roll Mode:
-
- - -
-
+ +
+
-
- - - @if (hasError('targetValue', 'required')) { - Target Value is required - } - @if (hasError('targetValue', 'min')) { - Target Value must be at least 1 - } - @if (hasError('targetValue', 'max')) { - Target Value must be at most 100 - } -
-
- -
-

- Win Chance: {{ winChance() | number: '1.0-2' }}% -

-

- Potential Win: - {{ - potentialWin() | currency: 'EUR' : 'symbol' : '1.2-2' - }} -

-
- - - -
- -
- @if (rolledValue() !== null) { -
- {{ rolledValue() }} -
- } +
+

Spielanleitung

+
    +
  • • Setze deinen Einsatz und Zielwert
  • +
  • • Wähle "Über Zielwert" oder "Unter Zielwert"
  • +
  • • Gewinne, wenn der Würfel zu deinen Gunsten fällt
  • +
  • • Höheres Risiko = höhere Belohnung
  • +
+
- - @if (rolledValue() !== null) { -
- @if (win()) { -

- You Won! Payout: {{ payout() | currency: 'EUR' : 'symbol' : '1.2-2' }} -

- } @else { -

You Lost.

- } -
- }
diff --git a/frontend/src/app/feature/game/dice/dice.component.ts b/frontend/src/app/feature/game/dice/dice.component.ts index fe50cf6..4967b78 100644 --- a/frontend/src/app/feature/game/dice/dice.component.ts +++ b/frontend/src/app/feature/game/dice/dice.component.ts @@ -11,6 +11,9 @@ import { DiceService } from './dice.service'; import { DiceDto, DiceResult } from './dice.model'; import { tap } from 'rxjs/operators'; import { UserService } from '@service/user.service'; +import { PlaySoundDirective } from '@shared/directives/play-sound.directive'; +import { DragSoundDirective } from '@shared/directives/drag-sound.directive'; +import { AudioService } from '@shared/services/audio.service'; type DiceFormGroup = FormGroup<{ betAmount: FormControl; @@ -21,13 +24,14 @@ type DiceFormGroup = FormGroup<{ @Component({ selector: 'app-dice', standalone: true, - imports: [CommonModule, ReactiveFormsModule], + imports: [CommonModule, ReactiveFormsModule, PlaySoundDirective, DragSoundDirective], templateUrl: './dice.component.html', }) export class DiceComponent implements OnInit { private readonly formBuilder = inject(FormBuilder); private readonly diceService = inject(DiceService); private readonly userService = inject(UserService); + private readonly audioService = inject(AudioService); rolledValue = signal(null); win = signal(null); @@ -49,23 +53,23 @@ export class DiceComponent implements OnInit { createDiceForm(): DiceFormGroup { return this.formBuilder.group({ - betAmount: new FormControl(1.0, { - validators: [Validators.required, Validators.min(0.01)], + betAmount: new FormControl(1, { + validators: [Validators.required, Validators.min(1)], nonNullable: true, }), rollOver: new FormControl(true, { validators: [Validators.required], nonNullable: true, }), - targetValue: new FormControl(50.5, { - validators: [Validators.required, Validators.min(1), Validators.max(100)], + targetValue: new FormControl(50, { + validators: [Validators.required, Validators.min(1), Validators.max(99)], nonNullable: true, }), }); } toggleRollMode(): void { - const currentMode = this.diceForm.get('rollOver')?.value; + const currentMode = this.diceForm.get('rollOver')?.value ?? true; this.diceForm.get('rollOver')?.setValue(!currentMode); } @@ -104,6 +108,11 @@ export class DiceComponent implements OnInit { this.rolledValue.set(result.rolledValue); this.win.set(result.win); this.payout.set(result.payout); + + if (result.win) { + this.audioService.playWinSound(); + } + this.userService.refreshCurrentUser(); }, error: (error) => { @@ -112,6 +121,29 @@ export class DiceComponent implements OnInit { }); } + setBetAmount(percentage: number): void { + const user = this.userService['authService'].currentUserValue; + if (!user) return; + + const balance = user.balance || 0; + + const newBet = Math.max(1, Math.floor(balance * percentage * 100) / 100); + + this.diceForm.get('betAmount')?.setValue(newBet); + this.calculateWinChanceAndPotentialWin(); + } + + getTrackGradient(): string { + const targetValue = this.diceForm.get('targetValue')?.value ?? 50; + const isRollOver = this.diceForm.get('rollOver')?.value ?? true; + + if (isRollOver) { + return `linear-gradient(to right, var(--color-accent-red) ${targetValue}%, var(--color-emerald) ${targetValue}%)`; + } else { + return `linear-gradient(to right, var(--color-accent-red) ${targetValue}%, var(--color-emerald) ${targetValue}%)`; + } + } + hasError(controlName: string, errorName: string): boolean { const control = this.diceForm.get(controlName); return control !== null && control.touched && control.hasError(errorName); diff --git a/frontend/src/app/shared/directives/drag-sound.directive.ts b/frontend/src/app/shared/directives/drag-sound.directive.ts new file mode 100644 index 0000000..332cacc --- /dev/null +++ b/frontend/src/app/shared/directives/drag-sound.directive.ts @@ -0,0 +1,39 @@ +import { Directive, ElementRef, HostListener, inject, Input, OnInit } from '@angular/core'; +import { AudioService } from '../services/audio.service'; +import { AbstractControl } from '@angular/forms'; + +@Directive({ + selector: '[appDragSound]', + standalone: true, +}) +export class DragSoundDirective implements OnInit { + private audioService = inject(AudioService); + private elementRef = inject(ElementRef); + private lastValue: number | null = null; + + @Input('appDragSound') formControl: AbstractControl | null = null; + + ngOnInit() { + if (this.formControl) { + this.lastValue = this.formControl.value; + + this.formControl.valueChanges.subscribe((newValue) => { + if (this.lastValue !== newValue) { + this.playSound(); + this.lastValue = newValue; + } + }); + } + } + + private playSound() { + this.audioService.playDragStepSound(); + } + + @HostListener('input') + onInput() { + if (!this.formControl) { + this.playSound(); + } + } +} diff --git a/frontend/src/app/shared/services/audio.service.ts b/frontend/src/app/shared/services/audio.service.ts index 720658e..53850c0 100644 --- a/frontend/src/app/shared/services/audio.service.ts +++ b/frontend/src/app/shared/services/audio.service.ts @@ -27,4 +27,15 @@ export class AudioService { audio.currentTime = 0; audio.play().catch((error) => console.error('Error playing win sound:', error)); } + + getDragSound(): HTMLAudioElement { + return this.getAudio('drag.mp3'); + } + + playDragStepSound(): void { + const audio = this.getAudio('drag.mp3'); + audio.currentTime = 0; + audio.volume = 0.5; + audio.play().catch((error) => console.error('Error playing drag step sound:', error)); + } } From a1997537eb268978caf3dada7dcab25171a97d09 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 21 May 2025 14:02:47 +0200 Subject: [PATCH 004/124] style(dice.component.html): Update layout of game instructions --- .../app/feature/game/dice/dice.component.html | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/feature/game/dice/dice.component.html b/frontend/src/app/feature/game/dice/dice.component.html index 8d1ae51..68a94ac 100644 --- a/frontend/src/app/feature/game/dice/dice.component.html +++ b/frontend/src/app/feature/game/dice/dice.component.html @@ -288,18 +288,18 @@ > Würfeln + +
+

Spielanleitung

+
    +
  • • Setze deinen Einsatz und Zielwert
  • +
  • • Wähle "Über Zielwert" oder "Unter Zielwert"
  • +
  • • Gewinne, wenn der Würfel zu deinen Gunsten fällt
  • +
  • • Höheres Risiko = höhere Belohnung
  • +
+
- -
-

Spielanleitung

-
    -
  • • Setze deinen Einsatz und Zielwert
  • -
  • • Wähle "Über Zielwert" oder "Unter Zielwert"
  • -
  • • Gewinne, wenn der Würfel zu deinen Gunsten fällt
  • -
  • • Höheres Risiko = höhere Belohnung
  • -
-
From da90a332dc90727d6114d21e6090b5ae55433e81 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Wed, 21 May 2025 14:07:03 +0200 Subject: [PATCH 005/124] style(dice.component.html): fix whitespace in HTML file --- frontend/src/app/feature/game/dice/dice.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/feature/game/dice/dice.component.html b/frontend/src/app/feature/game/dice/dice.component.html index 68a94ac..6cf5d12 100644 --- a/frontend/src/app/feature/game/dice/dice.component.html +++ b/frontend/src/app/feature/game/dice/dice.component.html @@ -288,7 +288,7 @@ > Würfeln - +

Spielanleitung