Merge pull request 'feat(slots): add slot machine component with styling and logic' (!170) from task/CAS-44/lootbox-selection into main
Reviewed-on: #170 Reviewed-by: Constantin Simonis <constantin@simonis.lol>
This commit is contained in:
commit
ba6f72fb97
10 changed files with 447 additions and 210 deletions
|
@ -1,91 +1,91 @@
|
||||||
package de.szut.casino.lootboxes;
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
import de.szut.casino.exceptionHandling.exceptions.InsufficientFundsException;
|
||||||
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
import de.szut.casino.exceptionHandling.exceptions.UserNotFoundException;
|
||||||
import de.szut.casino.user.UserEntity;
|
import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserService;
|
import de.szut.casino.user.UserService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class LootBoxController {
|
public class LootBoxController {
|
||||||
private final LootBoxRepository lootBoxRepository;
|
private final LootBoxRepository lootBoxRepository;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final LootBoxService lootBoxService;
|
private final LootBoxService lootBoxService;
|
||||||
|
|
||||||
public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) {
|
public LootBoxController(LootBoxRepository lootBoxRepository, UserService userService, LootBoxService lootBoxService) {
|
||||||
this.lootBoxRepository = lootBoxRepository;
|
this.lootBoxRepository = lootBoxRepository;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.lootBoxService = lootBoxService;
|
this.lootBoxService = lootBoxService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/lootboxes")
|
@GetMapping("/lootboxes")
|
||||||
public List<LootBoxEntity> getAllLootBoxes() {
|
public List<LootBoxEntity> getAllLootBoxes() {
|
||||||
return lootBoxRepository.findAll();
|
return lootBoxRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/lootboxes/{id}")
|
@PostMapping("/lootboxes/{id}")
|
||||||
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
|
public ResponseEntity<Object> purchaseLootBox(@PathVariable Long id) {
|
||||||
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
||||||
if (optionalLootBox.isEmpty()) {
|
if (optionalLootBox.isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
LootBoxEntity lootBox = optionalLootBox.get();
|
LootBoxEntity lootBox = optionalLootBox.get();
|
||||||
|
|
||||||
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
Optional<UserEntity> optionalUser = userService.getCurrentUser();
|
||||||
if (optionalUser.isEmpty()) {
|
if (optionalUser.isEmpty()) {
|
||||||
throw new UserNotFoundException();
|
throw new UserNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
UserEntity user = optionalUser.get();
|
UserEntity user = optionalUser.get();
|
||||||
|
|
||||||
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
|
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
|
||||||
throw new InsufficientFundsException();
|
throw new InsufficientFundsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
RewardEntity reward = lootBoxService.determineReward(lootBox);
|
RewardEntity reward = lootBoxService.determineReward(lootBox);
|
||||||
lootBoxService.handleBalance(user, lootBox, reward);
|
lootBoxService.handleBalance(user, lootBox, reward);
|
||||||
|
|
||||||
return ResponseEntity.ok(reward);
|
return ResponseEntity.ok(reward);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/lootboxes")
|
@PostMapping("/lootboxes")
|
||||||
public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
|
public ResponseEntity<Object> createLootbox(@RequestBody @Valid CreateLootBoxDto createLootBoxDto) {
|
||||||
List<RewardEntity> rewardEntities = new ArrayList<>();
|
List<RewardEntity> rewardEntities = new ArrayList<>();
|
||||||
|
|
||||||
for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) {
|
for (CreateRewardDto createRewardDto : createLootBoxDto.getRewards()) {
|
||||||
rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability()));
|
rewardEntities.add(new RewardEntity(createRewardDto.getValue(), createRewardDto.getProbability()));
|
||||||
}
|
}
|
||||||
|
|
||||||
LootBoxEntity lootBoxEntity = new LootBoxEntity(
|
LootBoxEntity lootBoxEntity = new LootBoxEntity(
|
||||||
createLootBoxDto.getName(),
|
createLootBoxDto.getName(),
|
||||||
createLootBoxDto.getPrice(),
|
createLootBoxDto.getPrice(),
|
||||||
rewardEntities
|
rewardEntities
|
||||||
);
|
);
|
||||||
|
|
||||||
this.lootBoxRepository.save(lootBoxEntity);
|
this.lootBoxRepository.save(lootBoxEntity);
|
||||||
|
|
||||||
return ResponseEntity.ok(lootBoxEntity);
|
return ResponseEntity.ok(lootBoxEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/lootboxes/{id}")
|
@DeleteMapping("/lootboxes/{id}")
|
||||||
public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) {
|
public ResponseEntity<Object> deleteLootbox(@PathVariable Long id) {
|
||||||
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
|
||||||
if (optionalLootBox.isEmpty()) {
|
if (optionalLootBox.isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
LootBoxEntity lootBox = optionalLootBox.get();
|
LootBoxEntity lootBox = optionalLootBox.get();
|
||||||
lootBoxRepository.delete(lootBox);
|
lootBoxRepository.delete(lootBox);
|
||||||
|
|
||||||
return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox"));
|
return ResponseEntity.ok(Collections.singletonMap("message", "successfully deleted lootbox"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
package de.szut.casino.lootboxes;
|
package de.szut.casino.lootboxes;
|
||||||
|
|
||||||
import de.szut.casino.user.UserEntity;
|
import de.szut.casino.user.UserEntity;
|
||||||
import de.szut.casino.user.UserRepository;
|
import de.szut.casino.user.UserRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class LootBoxService {
|
public class LootBoxService {
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
public LootBoxService(UserRepository userRepository) {
|
public LootBoxService(UserRepository userRepository) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
|
public boolean hasSufficientBalance(UserEntity user, BigDecimal price) {
|
||||||
return user.getBalance().compareTo(price) < 0;
|
return user.getBalance().compareTo(price) < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RewardEntity determineReward(LootBoxEntity lootBox) {
|
public RewardEntity determineReward(LootBoxEntity lootBox) {
|
||||||
double randomValue = Math.random();
|
double randomValue = Math.random();
|
||||||
BigDecimal cumulativeProbability = BigDecimal.ZERO;
|
BigDecimal cumulativeProbability = BigDecimal.ZERO;
|
||||||
|
|
||||||
for (RewardEntity reward : lootBox.getRewards()) {
|
for (RewardEntity reward : lootBox.getRewards()) {
|
||||||
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
|
cumulativeProbability = cumulativeProbability.add(reward.getProbability());
|
||||||
if (randomValue <= cumulativeProbability.doubleValue()) {
|
if (randomValue <= cumulativeProbability.doubleValue()) {
|
||||||
return reward;
|
return reward;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lootBox.getRewards().getLast();
|
return lootBox.getRewards().getLast();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
|
public void handleBalance(UserEntity user, LootBoxEntity lootBox, RewardEntity reward) {
|
||||||
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
|
user.setBalance(user.getBalance().subtract(lootBox.getPrice()));
|
||||||
user.setBalance(user.getBalance().add(reward.getValue()));
|
user.setBalance(user.getBalance().add(reward.getValue()));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,12 +54,13 @@ public class SlotService {
|
||||||
SpinResult spinResult = new SpinResult();
|
SpinResult spinResult = new SpinResult();
|
||||||
spinResult.setStatus(status.name().toLowerCase());
|
spinResult.setStatus(status.name().toLowerCase());
|
||||||
|
|
||||||
|
this.balanceService.subtractFunds(user, betAmount);
|
||||||
|
|
||||||
if (status == Status.WIN) {
|
if (status == Status.WIN) {
|
||||||
BigDecimal winAmount = betAmount.multiply(winSymbol.getPayoutMultiplier());
|
BigDecimal winAmount = betAmount.multiply(winSymbol.getPayoutMultiplier());
|
||||||
this.balanceService.addFunds(user, winAmount);
|
this.balanceService.addFunds(user, winAmount);
|
||||||
spinResult.setAmount(winAmount);
|
spinResult.setAmount(winAmount);
|
||||||
} else {
|
} else {
|
||||||
this.balanceService.subtractFunds(user, betAmount);
|
|
||||||
spinResult.setAmount(betAmount);
|
spinResult.setAmount(betAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||||
import { UserService } from '@service/user.service';
|
import { UserService } from '@service/user.service';
|
||||||
import { timer } from 'rxjs';
|
import { timer } from 'rxjs';
|
||||||
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
|
import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.component';
|
||||||
|
import { AuthService } from '@service/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blackjack',
|
selector: 'app-blackjack',
|
||||||
|
@ -34,6 +35,7 @@ import { DebtDialogComponent } from '@shared/components/debt-dialog/debt-dialog.
|
||||||
export default class BlackjackComponent implements OnInit {
|
export default class BlackjackComponent implements OnInit {
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private userService = inject(UserService);
|
private userService = inject(UserService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
private blackjackService = inject(BlackjackService);
|
private blackjackService = inject(BlackjackService);
|
||||||
|
|
||||||
dealerCards = signal<Card[]>([]);
|
dealerCards = signal<Card[]>([]);
|
||||||
|
@ -51,7 +53,8 @@ export default class BlackjackComponent implements OnInit {
|
||||||
debtAmount = signal(0);
|
debtAmount = signal(0);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.userService.getCurrentUser().subscribe((user) => {
|
// Subscribe to user updates for real-time balance changes
|
||||||
|
this.authService.userSubject.subscribe((user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
this.balance.set(user.balance);
|
this.balance.set(user.balance);
|
||||||
}
|
}
|
||||||
|
@ -84,9 +87,14 @@ export default class BlackjackComponent implements OnInit {
|
||||||
if (isGameOver) {
|
if (isGameOver) {
|
||||||
console.log('Game is over, state:', game.state);
|
console.log('Game is over, state:', game.state);
|
||||||
this.userService.refreshCurrentUser();
|
this.userService.refreshCurrentUser();
|
||||||
timer(1500).subscribe(() => {
|
|
||||||
this.showGameResult.set(true);
|
// Get the latest balance before showing the result dialog
|
||||||
console.log('Game result dialog shown after delay');
|
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({
|
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
|
||||||
next: (game) => {
|
next: (game) => {
|
||||||
this.updateGameState(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) {
|
if (user && user.balance < 0) {
|
||||||
this.debtAmount.set(Math.abs(user.balance));
|
this.debtAmount.set(Math.abs(user.balance));
|
||||||
this.showDebtDialog.set(true);
|
this.showDebtDialog.set(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isActionInProgress.set(false);
|
this.isActionInProgress.set(false);
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
|
@ -184,7 +196,6 @@ export default class BlackjackComponent implements OnInit {
|
||||||
onCloseGameResult(): void {
|
onCloseGameResult(): void {
|
||||||
console.log('Closing game result dialog');
|
console.log('Closing game result dialog');
|
||||||
this.showGameResult.set(false);
|
this.showGameResult.set(false);
|
||||||
this.userService.refreshCurrentUser();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCloseDebtDialog(): void {
|
onCloseDebtDialog(): void {
|
||||||
|
|
16
frontend/src/app/feature/game/slots/slots.component.css
Normal file
16
frontend/src/app/feature/game/slots/slots.component.css
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -1,50 +1,177 @@
|
||||||
<app-navbar></app-navbar>
|
<app-navbar></app-navbar>
|
||||||
|
|
||||||
<div>
|
<div class="container mx-auto px-4 py-6 space-y-8">
|
||||||
<h2>Payouts</h2>
|
<h1 class="text-3xl font-bold text-white mb-6">Spielautomaten</h1>
|
||||||
@if (slotInfo(); as info) {
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
@for (item of info | keyvalue; track item.key) {
|
|
||||||
<tr>
|
|
||||||
<td>{{ item.key }}</td>
|
|
||||||
<td>{{ item.value }}</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div>
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||||
<div class="grid grid-cols-3 gap-1">
|
<!-- Slot Machine Display -->
|
||||||
@for (row of slotResult().resultMatrix; track $index) {
|
<div class="lg:col-span-3 space-y-6 flex flex-col">
|
||||||
@for (cell of row; track $index) {
|
<div class="card">
|
||||||
<div class="text-center">{{ cell }}</div>
|
<!-- Slot Machine Top -->
|
||||||
}
|
<div class="p-6">
|
||||||
}
|
<div class="flex items-center justify-between">
|
||||||
|
<h2 class="text-xl font-bold text-white">Slot Machine</h2>
|
||||||
|
<div
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-emerald': slotResult().status === 'win',
|
||||||
|
'bg-accent-red': slotResult().status === 'lose',
|
||||||
|
'bg-deep-blue-light': slotResult().status === 'start',
|
||||||
|
}"
|
||||||
|
class="px-3 py-1 rounded-full text-sm font-semibold"
|
||||||
|
>
|
||||||
|
<span class="text-white">
|
||||||
|
{{
|
||||||
|
slotResult().status === 'win'
|
||||||
|
? 'Gewonnen!'
|
||||||
|
: slotResult().status === 'lose'
|
||||||
|
? 'Verloren'
|
||||||
|
: 'Bereit'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slot Display -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="bg-deep-blue-light rounded-lg p-4 shadow-inner mb-6">
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
@for (row of slotResult().resultMatrix; track $index) {
|
||||||
|
@for (cell of row; track $index) {
|
||||||
|
<div
|
||||||
|
class="bg-deep-blue-contrast rounded-lg shadow-md p-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span class="text-2xl font-bold" [ngClass]="getSymbolClass(cell)">{{
|
||||||
|
cell
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Result -->
|
||||||
|
<div class="text-center mb-6" *ngIf="slotResult().status === 'win'">
|
||||||
|
<div class="text-emerald text-xl font-bold">
|
||||||
|
+{{ slotResult().amount | currency: 'EUR' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 items-center justify-center">
|
||||||
|
<div class="flex items-center bg-deep-blue-light rounded-lg p-2 flex-1">
|
||||||
|
<label for="betAmount" class="text-text-secondary mr-3">Einsatz:</label>
|
||||||
|
<input
|
||||||
|
id="betAmount"
|
||||||
|
type="number"
|
||||||
|
[ngModel]="betAmount()"
|
||||||
|
(ngModelChange)="betAmount.set($event)"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
class="w-full bg-deep-blue-light text-white focus:outline-none focus:ring-1 focus:ring-emerald"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
(click)="spin()"
|
||||||
|
class="button-primary px-4 py-1.5 font-bold"
|
||||||
|
[ngClass]="{
|
||||||
|
'bg-gray-500 cursor-not-allowed': !hasEnoughBalance(),
|
||||||
|
}"
|
||||||
|
[disabled]="isSpinning || !hasEnoughBalance()"
|
||||||
|
>
|
||||||
|
@if (!isSpinning) {
|
||||||
|
<span>{{ hasEnoughBalance() ? 'SPIN' : 'Nicht genug Guthaben' }}</span>
|
||||||
|
} @else {
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin"
|
||||||
|
></div>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<!-- Game Info Panel -->
|
||||||
<p>
|
<div class="lg:col-span-1">
|
||||||
Game result: <strong>{{ slotResult().status | uppercase }}</strong>
|
<div class="card p-4">
|
||||||
</p>
|
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
|
||||||
<p>
|
<div class="space-y-4">
|
||||||
Amount: <strong>{{ slotResult().amount }}</strong>
|
<div class="flex justify-between items-center">
|
||||||
</p>
|
<span class="text-text-secondary">Kontostand:</span>
|
||||||
</div>
|
<span class="text-emerald">
|
||||||
|
<app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-text-secondary">Einsatz:</span>
|
||||||
|
<span [class]="betAmount() > 0 ? 'text-accent-red' : 'text-text-secondary'">
|
||||||
|
<app-animated-number [value]="betAmount()" [duration]="0.5"></app-animated-number>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||||
<label for="betAmount">Bet Amount: </label>
|
<button
|
||||||
<input
|
(click)="setBetAmount(0.1)"
|
||||||
id="betAmount"
|
class="button-primary py-2 text-sm"
|
||||||
type="number"
|
[disabled]="isSpinning"
|
||||||
[ngModel]="betAmount()"
|
>
|
||||||
(ngModelChange)="betAmount.set($event)"
|
10%
|
||||||
step="0.01"
|
</button>
|
||||||
min="0.01"
|
<button
|
||||||
/>
|
(click)="setBetAmount(0.25)"
|
||||||
</div>
|
class="button-primary py-2 text-sm"
|
||||||
|
[disabled]="isSpinning"
|
||||||
|
>
|
||||||
|
25%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="setBetAmount(0.5)"
|
||||||
|
class="button-primary py-2 text-sm"
|
||||||
|
[disabled]="isSpinning"
|
||||||
|
>
|
||||||
|
50%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
(click)="setBetAmount(1)"
|
||||||
|
class="button-primary py-2 text-sm"
|
||||||
|
[disabled]="isSpinning"
|
||||||
|
>
|
||||||
|
100%
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button (click)="spin()">SPIN</button>
|
<h3 class="text-lg font-semibold text-white mb-2">Auszahlungen:</h3>
|
||||||
|
|
||||||
|
@if (slotInfo(); as info) {
|
||||||
|
<ul class="space-y-1">
|
||||||
|
@for (item of info | keyvalue; track item.key) {
|
||||||
|
<li class="flex justify-between items-center py-1 border-b border-deep-blue-light">
|
||||||
|
<div class="bg-deep-blue-contrast px-2 py-1 rounded text-center w-12">
|
||||||
|
<span [ngClass]="getSymbolClass(item.key)">{{ item.key }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-emerald">{{ item.value }}x</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
} @else {
|
||||||
|
<div class="flex justify-center py-4">
|
||||||
|
<div
|
||||||
|
class="w-4 h-4 border-2 border-deep-blue-contrast border-t-emerald rounded-full animate-spin"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mt-4 pt-2">
|
||||||
|
<h4 class="text-sm font-semibold text-white mb-2">Spielregeln:</h4>
|
||||||
|
<ul class="text-text-secondary text-xs space-y-1">
|
||||||
|
<li>• Gewinne mit 3 gleichen Symbolen</li>
|
||||||
|
<li>• Höhere Symbole = höhere Gewinne</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||||
import { HttpClient } from '@angular/common/http';
|
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 { 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 {
|
interface SlotResult {
|
||||||
status: 'win' | 'lose' | 'blank' | 'start';
|
status: 'win' | 'lose' | 'blank' | 'start';
|
||||||
|
@ -13,12 +24,25 @@ interface SlotResult {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-slots',
|
selector: 'app-slots',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NavbarComponent, KeyValuePipe, UpperCasePipe, FormsModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NavbarComponent,
|
||||||
|
KeyValuePipe,
|
||||||
|
NgClass,
|
||||||
|
FormsModule,
|
||||||
|
CurrencyPipe,
|
||||||
|
AnimatedNumberComponent,
|
||||||
|
],
|
||||||
templateUrl: './slots.component.html',
|
templateUrl: './slots.component.html',
|
||||||
|
styleUrl: './slots.component.css',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export default class SlotsComponent implements OnInit {
|
export default class SlotsComponent implements OnInit, OnDestroy {
|
||||||
private httpClient: HttpClient = inject(HttpClient);
|
private httpClient: HttpClient = inject(HttpClient);
|
||||||
|
private userService = inject(UserService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private userSubscription: Subscription | undefined;
|
||||||
|
|
||||||
slotInfo = signal<Record<string, number> | null>(null);
|
slotInfo = signal<Record<string, number> | null>(null);
|
||||||
slotResult = signal<SlotResult>({
|
slotResult = signal<SlotResult>({
|
||||||
status: 'start',
|
status: 'start',
|
||||||
|
@ -29,21 +53,80 @@ export default class SlotsComponent implements OnInit {
|
||||||
['BELL', 'BELL', 'BELL'],
|
['BELL', 'BELL', 'BELL'],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
balance = signal<number>(0);
|
||||||
betAmount = signal<number>(1);
|
betAmount = signal<number>(1);
|
||||||
|
isSpinning = false;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => {
|
this.httpClient.get<Record<string, number>>('/backend/slots/info').subscribe((data) => {
|
||||||
this.slotInfo.set(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 {
|
spin(): void {
|
||||||
|
if (!this.hasEnoughBalance()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isSpinning = true;
|
||||||
|
const betAmount = this.betAmount();
|
||||||
|
|
||||||
|
this.userService.updateLocalBalance(-betAmount);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
betAmount: this.betAmount(),
|
betAmount: betAmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.httpClient.post<SlotResult>('/backend/slots/spin', payload).subscribe((result) => {
|
this.httpClient.post<SlotResult>('/backend/slots/spin', payload).subscribe({
|
||||||
this.slotResult.set(result);
|
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;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { LootBox, Reward } from 'app/model/LootBox';
|
||||||
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
|
||||||
import { UserService } from '@service/user.service';
|
import { UserService } from '@service/user.service';
|
||||||
import { User } from 'app/model/User';
|
import { User } from 'app/model/User';
|
||||||
|
import { AuthService } from '@service/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lootbox-opening',
|
selector: 'app-lootbox-opening',
|
||||||
|
@ -30,10 +31,11 @@ export default class LootboxOpeningComponent {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private lootboxService: LootboxService,
|
private lootboxService: LootboxService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
private authService: AuthService,
|
||||||
private cdr: ChangeDetectorRef
|
private cdr: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
this.loadLootbox();
|
this.loadLootbox();
|
||||||
this.userService.currentUser$.subscribe((user) => {
|
this.authService.userSubject.subscribe((user) => {
|
||||||
this.currentUser = user;
|
this.currentUser = user;
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,8 +5,9 @@ import { LootboxService } from '../services/lootbox.service';
|
||||||
import { LootBox } from 'app/model/LootBox';
|
import { LootBox } from 'app/model/LootBox';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { timeout } from 'rxjs';
|
import { timeout } from 'rxjs';
|
||||||
import { UserService } from '@service/user.service';
|
|
||||||
import { User } from 'app/model/User';
|
import { User } from 'app/model/User';
|
||||||
|
import { AuthService } from '@service/auth.service';
|
||||||
|
import { UserService } from '@service/user.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lootbox-selection',
|
selector: 'app-lootbox-selection',
|
||||||
|
@ -90,12 +91,13 @@ export default class LootboxSelectionComponent implements OnInit {
|
||||||
private lootboxService: LootboxService,
|
private lootboxService: LootboxService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cdr: ChangeDetectorRef,
|
private cdr: ChangeDetectorRef,
|
||||||
|
private authService: AuthService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.loadLootboxes();
|
this.loadLootboxes();
|
||||||
this.userService.currentUser$.subscribe((user) => {
|
this.authService.userSubject.subscribe((user) => {
|
||||||
this.currentUser = user;
|
this.currentUser = user;
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,38 +1,33 @@
|
||||||
import { inject, Injectable } from '@angular/core';
|
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 { AuthService } from '@service/auth.service';
|
||||||
|
import { User } from '../model/User';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UserService {
|
export class UserService {
|
||||||
public currentUserSubject = new BehaviorSubject<User | null>(null);
|
|
||||||
public currentUser$ = this.currentUserSubject.asObservable();
|
|
||||||
private http: HttpClient = inject(HttpClient);
|
|
||||||
private authService = inject(AuthService);
|
private authService = inject(AuthService);
|
||||||
|
|
||||||
public getCurrentUser(): Observable<User | null> {
|
/**
|
||||||
return this.http.get<User | null>('/backend/users/me').pipe(
|
* Updates the user's balance locally for immediate UI feedback
|
||||||
catchError(() => EMPTY),
|
* This should be called before a server-side balance change is made
|
||||||
tap((user) => this.currentUserSubject.next(user))
|
* The server update will be reflected when AuthService.loadCurrentUser() is called
|
||||||
);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
public refreshCurrentUser(): void {
|
|
||||||
this.getCurrentUser().subscribe();
|
|
||||||
this.authService.loadCurrentUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateLocalBalance(amount: number): void {
|
public updateLocalBalance(amount: number): void {
|
||||||
const currentUser = this.currentUserSubject.getValue();
|
const currentUser = this.authService.currentUserValue;
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
const updatedUser = {
|
const updatedUser: User = {
|
||||||
...currentUser,
|
...currentUser,
|
||||||
balance: currentUser.balance + amount,
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue