import { NgClass, NgIf, CurrencyPipe, CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; 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 { 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, FormsModule, CommonModule, NgIf, NgClass], templateUrl: './coinflip.component.html', styleUrl: './coinflip.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) export default class CoinflipComponent implements OnInit { currentBet = signal(10); balance = signal(0); gameInProgress = signal(false); isActionInProgress = signal(false); gameResult = signal(null); betInputValue = signal(10); errorMessage = signal(''); isInvalidBet = signal(false); @ViewChild('coinElement') coinElement?: ElementRef; audioService = inject(AudioService); authService = inject(AuthService); private http = inject(HttpClient); private coinflipSound?: HTMLAudioElement; ngOnInit(): void { // Subscribe to user updates for real-time balance changes this.authService.userSubject.subscribe((user) => { if (user) { 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; let value = Number(inputElement.value); // Reset invalid bet state this.isInvalidBet.set(false); // Enforce minimum bet of 1 if (value <= 0) { value = 1; } // Cap bet at available balance and show feedback if (value > this.balance()) { value = this.balance(); // Show visual feedback this.isInvalidBet.set(true); // Indicate the error briefly setTimeout(() => this.isInvalidBet.set(false), 800); // Update the input field directly to show the user the max value inputElement.value = String(value); } // Update signals 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}`); } /** * Validates input as the user types to prevent invalid values */ validateBetInput(event: KeyboardEvent) { // Allow navigation keys (arrows, delete, backspace, tab) const navigationKeys = ['ArrowLeft', 'ArrowRight', 'Delete', 'Backspace', 'Tab']; if (navigationKeys.includes(event.key)) { return; } // Only allow numbers if (!/^\d$/.test(event.key)) { event.preventDefault(); return; } // Get the value that would result after the keypress const input = event.target as HTMLInputElement; const currentValue = input.value; const cursorPosition = input.selectionStart || 0; const newValue = currentValue.substring(0, cursorPosition) + event.key + currentValue.substring(input.selectionEnd || cursorPosition); const numValue = Number(newValue); // Prevent values greater than balance 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 getResultClass() { if (!this.gameResult()) return ''; const result = this.gameResult(); const isWinner = result?.isWin || result?.win; return isWinner ? 'text-emerald-500' : 'text-accent-red'; } }