Compare commits

...

4 commits

Author SHA1 Message Date
7aefe67aa0
refactor(lottery): improve code structure and readability
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 38s
CI / Docker frontend validation (pull_request) Successful in 50s
CI / oxlint (pull_request) Successful in 27s
CI / prettier (pull_request) Failing after 28s
CI / Docker backend validation (pull_request) Successful in 1m23s
CI / Checkstyle Main (pull_request) Successful in 1m20s
CI / test-build (pull_request) Successful in 40s
2025-05-07 17:55:31 +02:00
a3f34e960b
feat(slots): update balance handling on spin results 2025-05-07 17:47:34 +02:00
93c5dc7fe3
style: format import statements for readability 2025-05-07 17:34:34 +02:00
4d76655e2f
style(slots): clean up unused button styling 2025-05-07 17:34:22 +02:00
10 changed files with 216 additions and 158 deletions

View file

@ -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<LootBoxEntity> getAllLootBoxes() {
return lootBoxRepository.findAll();
}
@PostMapping("/lootboxes/{id}")
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
Optional<UserEntity> 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<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
List<RewardEntity> 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<Object> deleteLootbox(@PathVariable Long id) {
Optional<LootBoxEntity> 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<LootBoxEntity> getAllLootBoxes() {
return lootBoxRepository.findAll();
}
@PostMapping("/lootboxes/{id}")
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
Optional<UserEntity> 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<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
List<RewardEntity> 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<Object> deleteLootbox(@PathVariable Long id) {
Optional<LootBoxEntity> 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"));
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -51,11 +51,19 @@ export default class BlackjackComponent implements OnInit {
debtAmount = signal(0);
ngOnInit(): void {
// Initial user load from server
this.userService.getCurrentUser().subscribe((user) => {
if (user) {
this.balance.set(user.balance);
}
});
// Subscribe to user updates for real-time balance changes
this.userService.currentUser$.subscribe((user) => {
if (user) {
this.balance.set(user.balance);
}
});
}
private updateGameState(game: BlackjackGame) {
@ -84,9 +92,19 @@ 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(() => {
this.userService.getCurrentUser().subscribe((user) => {
if (user) {
this.balance.set(user.balance);
// Show the result dialog after updating the balance
timer(500).subscribe(() => {
this.showGameResult.set(true);
console.log('Game result dialog shown after delay');
});
}
});
});
}
}
@ -165,12 +183,20 @@ export default class BlackjackComponent implements OnInit {
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => {
this.updateGameState(game);
this.userService.getCurrentUser().subscribe((user) => {
if (user && user.balance < 0) {
this.debtAmount.set(Math.abs(user.balance));
this.showDebtDialog.set(true);
}
// Wait a bit to ensure the backend has finished processing
timer(1000).subscribe(() => {
this.userService.getCurrentUser().subscribe((user) => {
if (user) {
this.balance.set(user.balance);
if (user.balance < 0) {
this.debtAmount.set(Math.abs(user.balance));
this.showDebtDialog.set(true);
}
}
});
});
this.isActionInProgress.set(false);
},
error: (error) => {
@ -184,7 +210,13 @@ export default class BlackjackComponent implements OnInit {
onCloseGameResult(): void {
console.log('Closing game result dialog');
this.showGameResult.set(false);
this.userService.refreshCurrentUser();
// Refresh the balance when dialog is closed and update local state
this.userService.getCurrentUser().subscribe((user) => {
if (user) {
this.balance.set(user.balance);
}
});
}
onCloseDebtDialog(): void {

View file

@ -1,15 +1,3 @@
/* Open button styling - Matches lootbox component style */
.open-btn {
background: linear-gradient(90deg, #4338ca 0%, #8b5cf6 100%);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.open-btn:hover {
background: linear-gradient(90deg, #4f46e5 0%, #a78bfa 100%);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3);
}
/* Symbol colors */
.symbol-BAR {
color: var(--color-accent-yellow);

View file

@ -74,9 +74,8 @@
<button
(click)="spin()"
class="px-4 py-1.5 font-bold rounded"
class="button-primary px-4 py-1.5 font-bold"
[ngClass]="{
'open-btn': hasEnoughBalance(),
'bg-gray-500 cursor-not-allowed': !hasEnoughBalance(),
}"
[disabled]="isSpinning || !hasEnoughBalance()"

View file

@ -109,17 +109,20 @@ export default class SlotsComponent implements OnInit, OnDestroy {
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;
},
});

View file

@ -1,4 +1,11 @@
import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
inject,
Input,
Output,
} from '@angular/core';
import { TransactionService } from '@service/transaction.service';
import { Observable } from 'rxjs';
import { AsyncPipe, CurrencyPipe, DatePipe, NgIf } from '@angular/common';

View file

@ -13,6 +13,19 @@ export class UserService {
private http: HttpClient = inject(HttpClient);
private authService = inject(AuthService);
constructor() {
// Initialize with the current user from AuthService if available
const currentUser = this.authService.getUser();
if (currentUser) {
this.currentUserSubject.next(currentUser);
}
// Subscribe to auth service user updates
this.authService.userSubject.subscribe(user => {
this.currentUserSubject.next(user);
});
}
public getCurrentUser(): Observable<User | null> {
return this.http.get<User | null>('/backend/users/me').pipe(
catchError(() => EMPTY),

View file

@ -10,6 +10,7 @@ import { RouterModule } from '@angular/router';
import { AuthService } from '@service/auth.service';
import { Subscription } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
import { UserService } from '@service/user.service';
@Component({
selector: 'app-navbar',
@ -21,9 +22,11 @@ import { AnimatedNumberComponent } from '@blackjack/components/animated-number/a
export class NavbarComponent implements OnInit, OnDestroy {
isMenuOpen = false;
private authService: AuthService = inject(AuthService);
private userService: UserService = inject(UserService);
isLoggedIn = this.authService.isLoggedIn();
private authSubscription!: Subscription;
private userSubscription!: Subscription;
public balance = signal(0);
ngOnInit() {
@ -32,10 +35,22 @@ export class NavbarComponent implements OnInit, OnDestroy {
this.balance.set(user?.balance ?? 0);
},
});
// Also subscribe to UserService for real-time balance updates
this.userSubscription = this.userService.currentUser$.subscribe({
next: (user) => {
if (user) {
this.balance.set(user.balance);
}
},
});
}
ngOnDestroy() {
this.authSubscription.unsubscribe();
if (this.userSubscription) {
this.userSubscription.unsubscribe();
}
}
logout() {