diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.css b/frontend/src/app/feature/game/coinflip/coinflip.component.css index ba7838c..a1548ab 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.css +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.css @@ -1,55 +1,106 @@ -/* Custom CSS for 3D Transformations and Coin Flip (using animation classes) */ +/* Custom CSS for 3D Transformations and Coin Flip */ @keyframes flipToHeads { 0% { - transform: rotateY(0deg) rotateX(0deg); + transform: rotateY(0); } 100% { - transform: rotateX(2880deg) rotateY(1440deg); + transform: rotateY(1800deg); /* End with heads facing up (even number of Y rotations) */ } } @keyframes flipToTails { 0% { - transform: rotateY(0deg) rotateX(0deg); /* Start with no rotation */ + transform: rotateY(0); } 100% { - transform: rotateX(-2880deg) rotateY(1620deg); /* Example: Reverse X rotation, and land on tails */ + transform: rotateY(1980deg); /* End with tails facing up (odd number of Y rotations) */ } } .coin-container { - width: 150px; /* Set the size of the coin */ - height: 150px; - perspective: 1000px; /* Adds 3D perspective */ + width: 180px; + height: 180px; + perspective: 1000px; + margin: 20px auto; } .coin { width: 100%; height: 100%; position: relative; - transform-style: preserve-3d; /* Crucial for 3D transformations */ + transform-style: preserve-3d; + transition: transform 0.01s; + transform: rotateY(0deg); + box-shadow: 0 0 30px rgba(0, 0, 0, 0.4); } .coin-side { - /* Common styles for both front and back */ width: 100%; height: 100%; position: absolute; - backface-visibility: hidden; /* Hide the back of both sides initially */ - border-radius: 50%; /* Make it circular */ + backface-visibility: hidden; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 24px; + box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2); + border: 8px solid rgba(255, 255, 255, 0.2); +} + +.front { + background: linear-gradient(45deg, #ffd700, #ffb700); + color: #333; + z-index: 2; } .back { - transform: rotateY( - 180deg - ); /* Rotate the back to face the opposite direction (Y-axis for horizontal flip) */ + transform: rotateY(180deg); + background: linear-gradient(45deg, #5a5a5a, #333333); + color: white; + z-index: 1; + /* Fix text display on the back of the coin */ + backface-visibility: hidden; } -/* Classes to trigger the specific animation */ +/* Fix reversed text on tails side by applying a counter-rotation to just the text */ +.back > span { + display: inline-block; + transform: rotateY(180deg); /* Counter-rotate the text so it appears correctly */ +} + +/* Animation classes */ .coin.animate-to-heads { - animation: flipToHeads 1s ease-in-out forwards; /* Apply the animation */ + animation: flipToHeads 1s ease-in-out forwards; } .coin.animate-to-tails { - animation: flipToTails 1s ease-in-out forwards; /* Apply the animation */ + animation: flipToTails 1s ease-in-out forwards; +} + +/* Make the buttons more responsive */ +button:not([disabled]) { + cursor: pointer; + transition: all 0.2s ease; +} + +button:not([disabled]):hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +button:not([disabled]):active { + transform: translateY(1px); +} + +/* Animation for results */ +@keyframes popIn { + 0% { transform: scale(0.8); opacity: 0; } + 70% { transform: scale(1.1); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } +} + +.result-text { + animation: popIn 0.5s ease-out forwards; } diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.html b/frontend/src/app/feature/game/coinflip/coinflip.component.html index 8d2c78a..57736f5 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.html +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.html @@ -1,42 +1,89 @@ -

Coinflip works!

- -
-
-
-
-
+
+
+
+ + @if (gameResult()) { +
+

+ {{ (gameResult()?.isWin || gameResult()?.win) ? 'You Won!' : 'You Lost' }} +

+

+ Coin landed on: {{ gameResult()?.coinSide === 'HEAD' ? 'HEAD' : 'TAILS' }} +

+ @if (gameResult()?.isWin || gameResult()?.win) { +

+ +{{ gameResult()?.payout | currency: 'EUR' }} +

+ } +
+ } + + + @if (errorMessage()) { +
+

{{ errorMessage() }}

+
+ } + + +
+
- Heads + HEAD
- Tails + TAILS
-
- -
+ +
-

Spiel Informationen

+

Game Information

+
- Aktuelle Wette: + Current Bet: - + € + +
+ + +
+ Your Balance: + + {{ balance() | currency: 'EUR' }}
+ @if (!gameInProgress()) {
@@ -46,22 +93,32 @@
} -
-
- - -
-
+ +
+ + +
+ + +
+

How to Play

+
    +
  • • Choose your bet amount
  • +
  • • Select Heads or Tails
  • +
  • • Win double your bet if correct
  • +
+
diff --git a/frontend/src/app/feature/game/coinflip/coinflip.component.ts b/frontend/src/app/feature/game/coinflip/coinflip.component.ts index 64bbeab..498d770 100644 --- a/frontend/src/app/feature/game/coinflip/coinflip.component.ts +++ b/frontend/src/app/feature/game/coinflip/coinflip.component.ts @@ -1,45 +1,38 @@ import { CurrencyPipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, ElementRef, inject, OnInit, signal, ViewChild } from '@angular/core'; import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component'; -import { GameInfoComponent } from '@blackjack/components/game-info/game-info.component'; +import { catchError, finalize } from 'rxjs/operators'; +import { of } from 'rxjs'; import { AuthService } from '@service/auth.service'; import { AudioService } from '@shared/services/audio.service'; +import { CoinflipGame, CoinflipRequest } from './models/coinflip.model'; @Component({ selector: 'app-coinflip', standalone: true, - imports: [AnimatedNumberComponent, CurrencyPipe], + imports: [AnimatedNumberComponent, CurrencyPipe, FormsModule], templateUrl: './coinflip.component.html', styleUrl: './coinflip.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) export default class CoinflipComponent implements OnInit { - currentBet = signal(0); + currentBet = signal(10); balance = signal(0); gameInProgress = signal(false); isActionInProgress = signal(false); + gameResult = signal(null); + betInputValue = signal(10); + errorMessage = signal(''); + + @ViewChild('coinElement') coinElement?: ElementRef; + audioService = inject(AudioService); authService = inject(AuthService); - private http = inject(HttpClient); - - setBetAmount(amount: number) { - this.currentBet.update((current) => current * amount); - } - - onNewGame(bet: number): void { - this.isActionInProgress.set(true); - this.audioService.playBetSound(); - this.currentBet.set(bet); - } - - betHeads() { - this.gameInProgress.set(true); - } - betTails() { - this.gameInProgress.set(true); - } + + private coinflipSound?: HTMLAudioElement; ngOnInit(): void { // Subscribe to user updates for real-time balance changes @@ -48,5 +41,151 @@ export default class CoinflipComponent implements OnInit { this.balance.set(user.balance); } }); + + // Initialize coinflip sound + this.coinflipSound = new Audio('/sounds/coinflip.mp3'); + } + + setBetAmount(percentage: number) { + const newBet = Math.floor(this.balance() * percentage); + this.betInputValue.set(newBet > 0 ? newBet : 1); + this.currentBet.set(this.betInputValue()); + } + + updateBet(event: Event) { + const inputElement = event.target as HTMLInputElement; + const value = Number(inputElement.value); + + if (value <= 0) { + this.betInputValue.set(1); + this.currentBet.set(1); + } else if (value > this.balance()) { + this.betInputValue.set(this.balance()); + this.currentBet.set(this.balance()); + } else { + this.betInputValue.set(value); + this.currentBet.set(value); + } + } + + betHeads() { + this.placeBet('HEAD'); + } + + betTails() { + this.placeBet('TAILS'); + } + + private placeBet(side: 'HEAD' | 'TAILS') { + if (this.gameInProgress() || this.isActionInProgress()) return; + + // Reset previous result + this.gameResult.set(null); + this.errorMessage.set(''); + + // Set game state + this.gameInProgress.set(true); + this.isActionInProgress.set(true); + + // Play bet sound + this.audioService.playBetSound(); + + // Create bet request + const request: CoinflipRequest = { + betAmount: this.currentBet(), + coinSide: side + }; + + // Call API + this.http.post('/backend/coinflip', request) + .pipe( + catchError(error => { + console.error('Error playing coinflip:', error); + + if (error.status === 400 && error.error.message.includes('insufficient')) { + this.errorMessage.set('Insufficient funds'); + } else { + this.errorMessage.set('An error occurred. Please try again.'); + } + + this.gameInProgress.set(false); + return of(null); + }), + finalize(() => { + this.isActionInProgress.set(false); + }) + ) + .subscribe(result => { + if (!result) return; + + console.log('API response:', result); + + // Fix potential property naming inconsistency from the backend + const fixedResult: CoinflipGame = { + isWin: result.isWin ?? result.win, + payout: result.payout, + coinSide: result.coinSide + }; + + console.log('Fixed result:', fixedResult); + + // Play coin flip animation and sound + this.playCoinFlipAnimation(fixedResult.coinSide); + + // Set result after animation completes + setTimeout(() => { + this.gameResult.set(fixedResult); + + // Update balance with new value from auth service + this.authService.loadCurrentUser(); + + // Play win sound if player won + if (fixedResult.isWin) { + this.audioService.playWinSound(); + } + + // Reset game state after showing result + setTimeout(() => { + this.gameInProgress.set(false); + }, 1500); + }, 1100); // Just after animation ends + }); + } + + private playCoinFlipAnimation(result: 'HEAD' | 'TAILS') { + if (!this.coinElement) return; + + const coinEl = this.coinElement.nativeElement; + + // Reset any existing animations + coinEl.classList.remove('animate-to-heads', 'animate-to-tails'); + + // Reset any inline styles from previous animations + coinEl.style.transform = ''; + + // Force a reflow to restart animation + void coinEl.offsetWidth; + + // Play flip sound + if (this.coinflipSound) { + this.coinflipSound.currentTime = 0; + this.coinflipSound.play().catch(err => console.error('Error playing sound:', err)); + } + + // Add appropriate animation class based on result + if (result === 'HEAD') { + coinEl.classList.add('animate-to-heads'); + } else { + coinEl.classList.add('animate-to-tails'); + } + + console.log(`Animation applied for result: ${result}`); + } + + getResultClass() { + if (!this.gameResult()) return ''; + const result = this.gameResult(); + const isWinner = result?.isWin || result?.win; + return isWinner ? 'text-emerald-500' : 'text-accent-red'; } } diff --git a/frontend/src/app/feature/game/coinflip/models/coinflip.model.ts b/frontend/src/app/feature/game/coinflip/models/coinflip.model.ts index 241fcf6..f87a3f9 100644 --- a/frontend/src/app/feature/game/coinflip/models/coinflip.model.ts +++ b/frontend/src/app/feature/game/coinflip/models/coinflip.model.ts @@ -1,5 +1,11 @@ export interface CoinflipGame { - isWin: boolean; + isWin?: boolean; + win?: boolean; payout: number; - coinSide: string; + coinSide: 'HEAD' | 'TAILS'; +} + +export interface CoinflipRequest { + betAmount: number; + coinSide: 'HEAD' | 'TAILS'; }