diff --git a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java index 540e4c5..5018914 100644 --- a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java +++ b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxController.java @@ -1,91 +1,91 @@ -package de.szut.casino.lootboxes; - -import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException; -import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; -import de.szut.casino.user.UserEntity; -import de.szut.casino.user.UserService; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -@RestController -public class LootBoxController { - private final LootBoxRepository lootBoxRepository; - private final UserService userService; - private final LootBoxService lootBoxService; - - public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) { - this.lootBoxRepository = lootBoxRepository; - this.userService = userService; - this.lootBoxService = lootBoxService; - } - - @GetMapping("/lootboxes") - public List getAllLootBoxes() { - return lootBoxRepository.findAll(); - } - - @PostMapping("/lootboxes/{id}") - public ResponseEntity purchaseLootBox(@PathVariable Long id) { - Optional optionalLootBox = lootBoxRepository.findById(id); - if (optionalLootBox.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - LootBoxEntity lootBox = optionalLootBox.get(); - - Optional optionalUser = userService.getCurrentUser(); - if (optionalUser.isEmpty()) { - throw new UserNotFoundException(); - } - - UserEntity user = optionalUser.get(); - - if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) { - throw new InsufficientFundsException(); - } - - RewardEntity reward = lootBoxService.determineReward(lootBox); - lootBoxService.handleBalance(user, lootBox, reward); - - return ResponseEntity.ok(reward); - } - - @PostMapping("/lootboxes") - public ResponseEntity createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) { - List rewardEntities = new ArrayList<>(); - - for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) { - rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability())); - } - - LootBoxEntity lootBoxEntity = new LootBoxEntity( - createLootBoxDto.getName(), - createLootBoxDto.getPrice(), - rewardEntities - ); - - this.lootBoxRepository.save(lootBoxEntity); - - return ResponseEntity.ok(lootBoxEntity); - } - - @DeleteMapping("/lootboxes/{id}") - public ResponseEntity deleteLootbox(@PathVariable Long id) { - Optional optionalLootBox = lootBoxRepository.findById(id); - if (optionalLootBox.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - LootBoxEntity lootBox = optionalLootBox.get(); - lootBoxRepository.delete(lootBox); - - return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox")); - } - -} +package de.szut.casino.lootboxes; + +import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException; +import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException; +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@RestController +public class LootBoxController { + private final LootBoxRepository lootBoxRepository; + private final UserService userService; + private final LootBoxService lootBoxService; + + public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) { + this.lootBoxRepository = lootBoxRepository; + this.userService = userService; + this.lootBoxService = lootBoxService; + } + + @GetMapping("/lootboxes") + public List getAllLootBoxes() { + return lootBoxRepository.findAll(); + } + + @PostMapping("/lootboxes/{id}") + public ResponseEntity purchaseLootBox(@PathVariable Long id) { + Optional optionalLootBox = lootBoxRepository.findById(id); + if (optionalLootBox.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + LootBoxEntity lootBox = optionalLootBox.get(); + + Optional optionalUser = userService.getCurrentUser(); + if (optionalUser.isEmpty()) { + throw new UserNotFoundException(); + } + + UserEntity user = optionalUser.get(); + + if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) { + throw new InsufficientFundsException(); + } + + RewardEntity reward = lootBoxService.determineReward(lootBox); + lootBoxService.handleBalance(user, lootBox, reward); + + return ResponseEntity.ok(reward); + } + + @PostMapping("/lootboxes") + public ResponseEntity createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) { + List rewardEntities = new ArrayList<>(); + + for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) { + rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability())); + } + + LootBoxEntity lootBoxEntity = new LootBoxEntity( + createLootBoxDto.getName(), + createLootBoxDto.getPrice(), + rewardEntities + ); + + this.lootBoxRepository.save(lootBoxEntity); + + return ResponseEntity.ok(lootBoxEntity); + } + + @DeleteMapping("/lootboxes/{id}") + public ResponseEntity deleteLootbox(@PathVariable Long id) { + Optional optionalLootBox = lootBoxRepository.findById(id); + if (optionalLootBox.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + LootBoxEntity lootBox = optionalLootBox.get(); + lootBoxRepository.delete(lootBox); + + return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox")); + } + +} diff --git a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxService.java b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxService.java index d80370e..647b69b 100644 --- a/backend/src/main/java/de/szut/casino/lootboxes/LootBoxService.java +++ b/backend/src/main/java/de/szut/casino/lootboxes/LootBoxService.java @@ -1,40 +1,40 @@ -package de.szut.casino.lootboxes; - -import de.szut.casino.user.UserEntity; -import de.szut.casino.user.UserRepository; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; - -@Service -public class LootBoxService { - private final UserRepository userRepository; - - public LootBoxService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - public boolean hasSufficientBalance(UserEntity user, BigDecimal price) { - return user.getBalance().compareTo(price) < 0; - } - - public RewardEntity determineReward(LootBoxEntity lootBox) { - double randomValue = Math.random(); - BigDecimal cumulativeProbability = BigDecimal.ZERO; - - for (RewardEntity reward : lootBox.getRewards()) { - cumulativeProbability = cumulativeProbability.add(reward.getProbability()); - if (randomValue <= cumulativeProbability.doubleValue()) { - return reward; - } - } - - return lootBox.getRewards().getLast(); - } - - public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) { - user.setBalance(user.getBalance().subtract(lootBox.getPrice())); - user.setBalance(user.getBalance().add(reward.getValue())); - userRepository.save(user); - } -} +package de.szut.casino.lootboxes; + +import de.szut.casino.user.UserEntity; +import de.szut.casino.user.UserRepository; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class LootBoxService { + private final UserRepository userRepository; + + public LootBoxService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public boolean hasSufficientBalance(UserEntity user, BigDecimal price) { + return user.getBalance().compareTo(price) < 0; + } + + public RewardEntity determineReward(LootBoxEntity lootBox) { + double randomValue = Math.random(); + BigDecimal cumulativeProbability = BigDecimal.ZERO; + + for (RewardEntity reward : lootBox.getRewards()) { + cumulativeProbability = cumulativeProbability.add(reward.getProbability()); + if (randomValue <= cumulativeProbability.doubleValue()) { + return reward; + } + } + + return lootBox.getRewards().getLast(); + } + + public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) { + user.setBalance(user.getBalance().subtract(lootBox.getPrice())); + user.setBalance(user.getBalance().add(reward.getValue())); + userRepository.save(user); + } +} diff --git a/backend/src/main/java/de/szut/casino/slots/SlotService.java b/backend/src/main/java/de/szut/casino/slots/SlotService.java index 4bdaa90..7905636 100644 --- a/backend/src/main/java/de/szut/casino/slots/SlotService.java +++ b/backend/src/main/java/de/szut/casino/slots/SlotService.java @@ -54,12 +54,13 @@ public class SlotService { SpinResult spinResult = new SpinResult(); spinResult.setStatus(status.name().toLowerCase()); + this.balanceService.subtractFunds(user, betAmount); + if (status == Status.WIN) { BigDecimal winAmount = betAmount.multiply(winSymbol.getPayoutMultiplier()); this.balanceService.addFunds(user, winAmount); spinResult.setAmount(winAmount); } else { - this.balanceService.subtractFunds(user, betAmount); spinResult.setAmount(betAmount); } diff --git a/frontend/src/app/feature/game/blackjack/blackjack.component.ts b/frontend/src/app/feature/game/blackjack/blackjack.component.ts index 2763b55..c21a012 100644 --- a/frontend/src/app/feature/game/blackjack/blackjack.component.ts +++ b/frontend/src/app/feature/game/blackjack/blackjack.component.ts @@ -14,6 +14,7 @@ import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { UserService } from '@service/user.service'; import { timer } from 'rxjs'; import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component'; +import { AuthService } from '@service/auth.service'; @Component({ selector: 'app-blackjack', @@ -34,6 +35,7 @@ import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog. export default class BlackjackComponent implements OnInit { private router = inject(Router); private userService = inject(UserService); + private authService = inject(AuthService); private blackjackService = inject(BlackjackService); dealerCards = signal([]); @@ -51,7 +53,8 @@ export default class BlackjackComponent implements OnInit { debtAmount = signal(0); ngOnInit(): void { - this.userService.getCurrentUser().subscribe((user) => { + // Subscribe to user updates for real-time balance changes + this.authService.userSubject.subscribe((user) => { if (user) { this.balance.set(user.balance); } @@ -84,9 +87,14 @@ export default class BlackjackComponent implements OnInit { if (isGameOver) { console.log('Game is over, state:', game.state); this.userService.refreshCurrentUser(); - timer(1500).subscribe(() => { - this.showGameResult.set(true); - console.log('Game result dialog shown after delay'); + + // Get the latest balance before showing the result dialog + timer(1000).subscribe(() => { + // Show the result dialog after refreshing user data + timer(500).subscribe(() => { + this.showGameResult.set(true); + console.log('Game result dialog shown after delay'); + }); }); } } @@ -165,12 +173,16 @@ export default class BlackjackComponent implements OnInit { this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ next: (game) => { this.updateGameState(game); - this.userService.getCurrentUser().subscribe((user) => { + + // Wait a bit to ensure the backend has finished processing + timer(1000).subscribe(() => { + const user = this.authService.currentUserValue; if (user && user.balance < 0) { this.debtAmount.set(Math.abs(user.balance)); this.showDebtDialog.set(true); } }); + this.isActionInProgress.set(false); }, error: (error) => { @@ -184,7 +196,6 @@ export default class BlackjackComponent implements OnInit { onCloseGameResult(): void { console.log('Closing game result dialog'); this.showGameResult.set(false); - this.userService.refreshCurrentUser(); } onCloseDebtDialog(): void { diff --git a/frontend/src/app/feature/game/slots/slots.component.css b/frontend/src/app/feature/game/slots/slots.component.css new file mode 100644 index 0000000..c1fa452 --- /dev/null +++ b/frontend/src/app/feature/game/slots/slots.component.css @@ -0,0 +1,16 @@ +/* Symbol colors */ +.symbol-BAR { + color: var(--color-accent-yellow); +} +.symbol-SEVEN { + color: var(--color-accent-red); +} +.symbol-BELL { + color: var(--color-accent-purple); +} +.symbol-CHERRY { + color: #ec4899; +} +.symbol-LEMON { + color: #a3e635; +} diff --git a/frontend/src/app/feature/game/slots/slots.component.html b/frontend/src/app/feature/game/slots/slots.component.html index b6d9318..c5929df 100644 --- a/frontend/src/app/feature/game/slots/slots.component.html +++ b/frontend/src/app/feature/game/slots/slots.component.html @@ -1,50 +1,177 @@ -
-

Payouts

- @if (slotInfo(); as info) { - - - @for (item of info | keyvalue; track item.key) { - - - - - } - -
{{ item.key }}{{ item.value }}
- } +
+

Spielautomaten

-
-
- @for (row of slotResult().resultMatrix; track $index) { - @for (cell of row; track $index) { -
{{ cell }}
- } - } +
+ +
+
+ +
+
+

Slot Machine

+
+ + {{ + slotResult().status === 'win' + ? 'Gewonnen!' + : slotResult().status === 'lose' + ? 'Verloren' + : 'Bereit' + }} + +
+
+
+ + +
+
+
+ @for (row of slotResult().resultMatrix; track $index) { + @for (cell of row; track $index) { +
+ {{ + cell + }} +
+ } + } +
+
+ + +
+
+ +{{ slotResult().amount | currency: 'EUR' }} +
+
+ + +
+
+ + +
+ + +
+
+
-
-

- Game result: {{ slotResult().status | uppercase }} -

-

- Amount: {{ slotResult().amount }} -

-
+ +
+
+

Spiel Informationen

+
+
+ Kontostand: + + + +
+
+ Einsatz: + + + +
-
- - -
+
+ + + + +
- +

Auszahlungen:

+ + @if (slotInfo(); as info) { +
    + @for (item of info | keyvalue; track item.key) { +
  • +
    + {{ item.key }} +
    + {{ item.value }}x +
  • + } +
+ } @else { +
+
+
+ } + +
+

Spielregeln:

+
    +
  • • Gewinne mit 3 gleichen Symbolen
  • +
  • • Höhere Symbole = höhere Gewinne
  • +
+
+
+
+
diff --git a/frontend/src/app/feature/game/slots/slots.component.ts b/frontend/src/app/feature/game/slots/slots.component.ts index ad73a03..71fa482 100644 --- a/frontend/src/app/feature/game/slots/slots.component.ts +++ b/frontend/src/app/feature/game/slots/slots.component.ts @@ -1,8 +1,19 @@ -import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, + OnDestroy, + signal, +} from '@angular/core'; import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { HttpClient } from '@angular/common/http'; -import { KeyValuePipe, UpperCasePipe } from '@angular/common'; +import { CommonModule, KeyValuePipe, NgClass, CurrencyPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import { UserService } from '@service/user.service'; +import { Subscription } from 'rxjs'; +import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component'; +import { AuthService } from '@service/auth.service'; interface SlotResult { status: 'win' | 'lose' | 'blank' | 'start'; @@ -13,12 +24,25 @@ interface SlotResult { @Component({ selector: 'app-slots', standalone: true, - imports: [NavbarComponent, KeyValuePipe, UpperCasePipe, FormsModule], + imports: [ + CommonModule, + NavbarComponent, + KeyValuePipe, + NgClass, + FormsModule, + CurrencyPipe, + AnimatedNumberComponent, + ], templateUrl: './slots.component.html', + styleUrl: './slots.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export default class SlotsComponent implements OnInit { +export default class SlotsComponent implements OnInit, OnDestroy { private httpClient: HttpClient = inject(HttpClient); + private userService = inject(UserService); + private authService = inject(AuthService); + private userSubscription: Subscription | undefined; + slotInfo = signal | null>(null); slotResult = signal({ status: 'start', @@ -29,21 +53,80 @@ export default class SlotsComponent implements OnInit { ['BELL', 'BELL', 'BELL'], ], }); + + balance = signal(0); betAmount = signal(1); + isSpinning = false; ngOnInit(): void { this.httpClient.get>('/backend/slots/info').subscribe((data) => { this.slotInfo.set(data); }); + + this.userSubscription = this.authService.userSubject.subscribe((user) => { + this.balance.set(user?.balance ?? 0); + }); + + this.userService.refreshCurrentUser(); + } + + ngOnDestroy(): void { + if (this.userSubscription) { + this.userSubscription.unsubscribe(); + } + } + + getSymbolClass(symbol: string): string { + return `symbol-${symbol}`; + } + + hasEnoughBalance(): boolean { + return this.balance() >= this.betAmount(); + } + + setBetAmount(percentage: number): void { + const calculatedBet = Math.floor(this.balance() * percentage * 100) / 100; + const minimumBet = 0.01; + + const newBet = Math.max(minimumBet, Math.min(calculatedBet, this.balance())); + + this.betAmount.set(newBet); } spin(): void { + if (!this.hasEnoughBalance()) { + return; + } + + this.isSpinning = true; + const betAmount = this.betAmount(); + + this.userService.updateLocalBalance(-betAmount); + const payload = { - betAmount: this.betAmount(), + betAmount: betAmount, }; - this.httpClient.post('/backend/slots/spin', payload).subscribe((result) => { - this.slotResult.set(result); + this.httpClient.post('/backend/slots/spin', payload).subscribe({ + next: (result) => { + setTimeout(() => { + this.slotResult.set(result); + + if (result.status === 'win') { + this.userService.updateLocalBalance(result.amount); + } + + this.userService.refreshCurrentUser(); + + this.isSpinning = false; + }, 1500); + }, + error: (err) => { + console.error('Error spinning slot machine:', err); + this.userService.updateLocalBalance(betAmount); + this.userService.refreshCurrentUser(); + this.isSpinning = false; + }, }); } } 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 3d84d9d..97e5200 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 @@ -6,6 +6,7 @@ import { LootBox, Reward } from 'app/model/LootBox'; import { NavbarComponent } from '@shared/components/navbar/navbar.component'; import { UserService } from '@service/user.service'; import { User } from 'app/model/User'; +import { AuthService } from '@service/auth.service'; @Component({ selector: 'app-lootbox-opening', @@ -30,10 +31,11 @@ export default class LootboxOpeningComponent { private router: Router, private lootboxService: LootboxService, private userService: UserService, + private authService: AuthService, private cdr: ChangeDetectorRef ) { this.loadLootbox(); - this.userService.currentUser$.subscribe((user) => { + this.authService.userSubject.subscribe((user) => { this.currentUser = user; this.cdr.detectChanges(); }); diff --git a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts index 747e29e..dc39869 100644 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts +++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts @@ -5,8 +5,9 @@ import { LootboxService } from '../services/lootbox.service'; import { LootBox } from 'app/model/LootBox'; import { Router } from '@angular/router'; import { timeout } from 'rxjs'; -import { UserService } from '@service/user.service'; import { User } from 'app/model/User'; +import { AuthService } from '@service/auth.service'; +import { UserService } from '@service/user.service'; @Component({ selector: 'app-lootbox-selection', @@ -90,12 +91,13 @@ export default class LootboxSelectionComponent implements OnInit { private lootboxService: LootboxService, private router: Router, private cdr: ChangeDetectorRef, + private authService: AuthService, private userService: UserService ) {} ngOnInit(): void { this.loadLootboxes(); - this.userService.currentUser$.subscribe((user) => { + this.authService.userSubject.subscribe((user) => { this.currentUser = user; this.cdr.detectChanges(); }); diff --git a/frontend/src/app/service/user.service.ts b/frontend/src/app/service/user.service.ts index d7199b3..e126fc4 100644 --- a/frontend/src/app/service/user.service.ts +++ b/frontend/src/app/service/user.service.ts @@ -1,38 +1,33 @@ import { inject, Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, catchError, EMPTY, Observable, tap } from 'rxjs'; -import { User } from '../model/User'; import { AuthService } from '@service/auth.service'; +import { User } from '../model/User'; @Injectable({ providedIn: 'root', }) export class UserService { - public currentUserSubject = new BehaviorSubject(null); - public currentUser$ = this.currentUserSubject.asObservable(); - private http: HttpClient = inject(HttpClient); private authService = inject(AuthService); - public getCurrentUser(): Observable { - return this.http.get('/backend/users/me').pipe( - catchError(() => EMPTY), - tap((user) => this.currentUserSubject.next(user)) - ); - } - - public refreshCurrentUser(): void { - this.getCurrentUser().subscribe(); - this.authService.loadCurrentUser(); - } - + /** + * Updates the user's balance locally for immediate UI feedback + * This should be called before a server-side balance change is made + * The server update will be reflected when AuthService.loadCurrentUser() is called + */ public updateLocalBalance(amount: number): void { - const currentUser = this.currentUserSubject.getValue(); + const currentUser = this.authService.currentUserValue; if (currentUser) { - const updatedUser = { + const updatedUser: User = { ...currentUser, balance: currentUser.balance + amount, }; - this.currentUserSubject.next(updatedUser); + this.authService.userSubject.next(updatedUser); } } + + /** + * Refreshes the current user's data from the server + */ + public refreshCurrentUser(): void { + this.authService.loadCurrentUser(); + } }