diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.html b/frontend/src/app/feature/game/blackjack/blackjack.component.html index debbb75..62d5489 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.html +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.html @@ -5,10 +5,22 @@
+ + + @if (isActionInProgress()) { +
+
+
+ {{ currentAction() }} +
+
+ } + @if (gameInProgress()) {
diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 57a04fc..5b75bd6 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -42,6 +42,10 @@ export default class BlackjackComponent { gameInProgress = signal(false); gameState = signal('IN_PROGRESS'); showGameResult = signal(false); + + // Add loading state trackers + isActionInProgress = signal(false); + currentAction = signal(''); constructor() { this.refreshUserBalance(); @@ -89,64 +93,85 @@ export default class BlackjackComponent { } onNewGame(bet: number): void { + this.isActionInProgress.set(true); + this.currentAction.set('Spiel wird gestartet...'); + this.blackjackService.startGame(bet).subscribe({ next: (game) => { this.updateGameState(game); this.refreshUserBalance(); + this.isActionInProgress.set(false); }, error: (error) => { console.error('Failed to start game:', error); + this.isActionInProgress.set(false); }, }); } onHit(): void { - if (!this.currentGameId()) return; + if (!this.currentGameId() || this.isActionInProgress()) return; + this.isActionInProgress.set(true); + this.currentAction.set('Karte wird gezogen...'); + this.blackjackService.hit(this.currentGameId()!).subscribe({ next: (game) => { this.updateGameState(game); + this.isActionInProgress.set(false); }, error: (error) => { console.error('Failed to hit:', error); this.handleGameError(error); + this.isActionInProgress.set(false); }, }); } onStand(): void { - if (!this.currentGameId()) return; + if (!this.currentGameId() || this.isActionInProgress()) return; if (this.gameState() !== 'IN_PROGRESS') { console.log('Cannot stand: game is not in progress'); return; } + this.isActionInProgress.set(true); + this.currentAction.set('Dealer zieht Karten...'); + this.blackjackService.stand(this.currentGameId()!).subscribe({ next: (game) => { this.updateGameState(game); + this.isActionInProgress.set(false); }, error: (error) => { console.error('Failed to stand:', error); this.handleGameError(error); + this.isActionInProgress.set(false); }, }); } onDoubleDown(): void { - if (!this.currentGameId()) return; + if (!this.currentGameId() || this.isActionInProgress()) return; if (this.gameState() !== 'IN_PROGRESS' || this.playerCards().length !== 2) { + console.log('Cannot double down: game is not in progress or more than 2 cards'); return; } + this.isActionInProgress.set(true); + this.currentAction.set('Einsatz wird verdoppelt...'); + this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ next: (game) => { this.updateGameState(game); + this.isActionInProgress.set(false); }, error: (error) => { console.error('Failed to double down:', error); this.handleGameError(error); + this.isActionInProgress.set(false); }, }); } diff --git a/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts b/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts index 89517a1..990b3c8 100644 --- a/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/dealer-hand/dealer-hand.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { PlayingCardComponent } from '../playing-card/playing-card.component'; import { Card } from '../../models/blackjack.model'; @@ -13,11 +13,12 @@ import { Card } from '../../models/blackjack.model';
@if (cards.length > 0) { - @for (card of cards; track card) { + @for (card of cardsWithState; track card.id) { } } @else { @@ -31,6 +32,33 @@ import { Card } from '../../models/blackjack.model'; `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DealerHandComponent { +export class DealerHandComponent implements OnChanges { @Input() cards: Card[] = []; + cardsWithState: (Card & { isNew: boolean; id: string })[] = []; + + private lastCardCount = 0; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['cards']) { + this.updateCardsWithState(); + } + } + + private updateCardsWithState(): void { + const newCards = this.cards.length > this.lastCardCount; + + this.cardsWithState = this.cards.map((card, index) => { + // Consider a card new if it's added after the initial state and is the latest card + const isNew = newCards && index >= this.lastCardCount; + + return { + ...card, + isNew, + // Generate a unique ID to help Angular track the cards + id: `${card.suit}-${card.rank}-${index}` + }; + }); + + this.lastCardCount = this.cards.length; + } } diff --git a/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts b/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts index 3e75315..899417f 100644 --- a/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/game-controls/game-controls.component.ts @@ -19,28 +19,48 @@ import { Card } from '../../models/blackjack.model';
@@ -52,6 +72,7 @@ import { Card } from '../../models/blackjack.model'; export class GameControlsComponent { @Input() playerCards: Card[] = []; @Input() gameState: string = 'IN_PROGRESS'; + @Input() isActionInProgress: boolean = false; @Output() hit = new EventEmitter(); @Output() stand = new EventEmitter(); diff --git a/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts b/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts index e35bf73..f6b6091 100644 --- a/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/game-info/game-info.component.ts @@ -86,10 +86,15 @@ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angula
@@ -101,6 +106,7 @@ export class GameInfoComponent implements OnChanges { @Input() balance = 0; @Input() currentBet = 0; @Input() gameInProgress = false; + @Input() isActionInProgress = false; @Output() newGame = new EventEmitter(); betForm: FormGroup; diff --git a/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts b/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts index e158f67..2b27fee 100644 --- a/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/player-hand/player-hand.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { PlayingCardComponent } from '../playing-card/playing-card.component'; import { Card } from '../../models/blackjack.model'; @@ -15,11 +15,12 @@ import { Card } from '../../models/blackjack.model'; class="flex justify-center gap-4 min-h-[160px] p-4 border-2 border-emerald-400 rounded-lg" > @if (cards.length > 0) { - @for (card of cards; track card) { + @for (card of cardsWithState; track card.id) { } } @else { @@ -33,6 +34,33 @@ import { Card } from '../../models/blackjack.model'; `, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PlayerHandComponent { +export class PlayerHandComponent implements OnChanges { @Input() cards: Card[] = []; + cardsWithState: (Card & { isNew: boolean; id: string })[] = []; + + private lastCardCount = 0; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['cards']) { + this.updateCardsWithState(); + } + } + + private updateCardsWithState(): void { + const newCards = this.cards.length > this.lastCardCount; + + this.cardsWithState = this.cards.map((card, index) => { + // Consider a card new if it's added after the initial state and is the latest card + const isNew = newCards && index >= this.lastCardCount; + + return { + ...card, + isNew, + // Generate a unique ID to help Angular track the cards + id: `${card.suit}-${card.rank}-${index}` + }; + }); + + this.lastCardCount = this.cards.length; + } } diff --git a/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts b/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts index 186ac9b..e006bd4 100644 --- a/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts +++ b/frontend/src/app/feature/game/blackjack/components/playing-card/playing-card.component.ts @@ -1,6 +1,7 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, AfterViewInit, ElementRef, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { suitSymbols, Suit } from '../../models/blackjack.model'; +import { gsap } from 'gsap'; @Component({ selector: 'app-playing-card', @@ -8,31 +9,90 @@ import { suitSymbols, Suit } from '../../models/blackjack.model'; imports: [CommonModule], template: `
@if (!hidden) { - {{ getDisplayRank(rank) }} + {{ getDisplayRank(rank) }} } @if (!hidden) { {{ getSuitSymbol(suit) }} } @if (!hidden) { - {{ + {{ getDisplayRank(rank) }} }
`, + styles: [` + .card-element { + transform-style: preserve-3d; + backface-visibility: hidden; + } + `], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PlayingCardComponent { +export class PlayingCardComponent implements AfterViewInit, OnChanges { @Input({ required: true }) rank!: string; @Input({ required: true }) suit!: Suit; @Input({ required: true }) hidden!: boolean; + @Input() isNew: boolean = false; + + constructor(private elementRef: ElementRef) {} + + get isRedSuit(): boolean { + return this.suit === 'HEARTS' || this.suit === 'DIAMONDS'; + } + + ngAfterViewInit(): void { + if (this.isNew) { + this.animateNewCard(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['hidden'] && !changes['hidden'].firstChange) { + this.animateCardFlip(); + } + } + + private animateNewCard(): void { + const cardElement = this.elementRef.nativeElement.querySelector('.card-element'); + gsap.fromTo( + cardElement, + { + y: -100, + opacity: 0, + rotation: -10, + scale: 0.7 + }, + { + y: 0, + opacity: 1, + rotation: 0, + scale: 1, + duration: 0.5, + ease: 'power2.out' + } + ); + } + + private animateCardFlip(): void { + const cardElement = this.elementRef.nativeElement.querySelector('.card-element'); + gsap.to(cardElement, { + rotationY: 180, + duration: 0.3, + onComplete: () => { + gsap.set(cardElement, { rotationY: 0 }); + } + }); + } protected getSuitSymbol(suit: Suit): string { return suitSymbols[suit];