Compare commits

...

38 commits

Author SHA1 Message Date
28f7b15d4c
refactor: remove unnecessary comments and variables
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Failing after 23s
CI / eslint (pull_request) Successful in 1m41s
CI / test-build (pull_request) Successful in 1m53s
2025-04-03 10:04:28 +02:00
4b70a4ac4a
feat(blackjack): add animated number component and usage 2025-04-03 10:04:28 +02:00
a2f1a40931
Merge pull request 'task/CAS-50/add_rest_blackjack_logic_with_frontend_animations' (!121) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 51s
Reviewed-on: #121
2025-04-02 11:09:07 +00:00
eb5b94c7bb
Merge pull request 'fix(deps): update dependency ajv-formats to v3' (!120) from renovate/major-dependencies-(major-and-minor) into main
All checks were successful
Release / Release (push) Successful in 58s
Reviewed-on: #120
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 11:03:59 +00:00
faa0a1495b
fix(deps): update dependency ajv-formats to v3
All checks were successful
CI / Get Changed Files (pull_request) Successful in 8s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 22s
CI / eslint (pull_request) Successful in 1m36s
CI / test-build (pull_request) Successful in 1m42s
2025-04-02 10:42:58 +00:00
823cb88807
Merge pull request 'Update the stripe api' (!119) from fix-renovate into main
All checks were successful
Release / Release (push) Successful in 56s
Reviewed-on: #119
Reviewed-by: Phan Huy Tran <ptran@noreply.localhost>
2025-04-02 10:21:33 +00:00
0aa7ad1031
style: Fix missing newline at end of files
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 1m3s
CI / prettier (pull_request) Successful in 51s
CI / test-build (pull_request) Successful in 1m23s
CI / Checkstyle Main (pull_request) Successful in 3m40s
2025-04-02 12:17:35 +02:00
b1b8c939a6
Merge branch 'main' into fix-renovate
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Failing after 38s
CI / eslint (pull_request) Successful in 1m1s
CI / test-build (pull_request) Successful in 1m30s
2025-04-02 12:15:23 +02:00
6182ff717f
feat(deposit): enhance payment session handling and error logging
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 27s
CI / Checkstyle Main (pull_request) Failing after 1m0s
CI / eslint (pull_request) Successful in 1m54s
CI / test-build (pull_request) Successful in 2m2s
2025-04-02 12:12:13 +02:00
4c3f42d347
Merge pull request 'fix(deps): update dependency ajv to v8.17.1' (!117) from renovate/dependencies-(major-and-minor) into main
All checks were successful
Release / Release (push) Successful in 1m10s
Reviewed-on: #117
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 10:03:53 +00:00
9981ebc9d1
fix(deps): update dependencies (major and minor)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Successful in 30s
CI / test-build (pull_request) Successful in 37s
CI / prettier (pull_request) Successful in 1m2s
CI / Checkstyle Main (pull_request) Failing after 2m20s
2025-04-02 10:01:44 +00:00
ab80f5d285
fix(deps): update dependency ajv to v8.17.1
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 31s
CI / test-build (pull_request) Successful in 40s
CI / prettier (pull_request) Successful in 45s
2025-04-02 10:01:37 +00:00
ac6e3be52c
Merge pull request 'Add all the renovate changes' (!115) from renovate-test into main
All checks were successful
Release / Release (push) Successful in 58s
Reviewed-on: #115
2025-04-02 10:00:20 +00:00
3c64b9ec8b
build: update stripe-java dependency version to 20.136.0
All checks were successful
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / prettier (pull_request) Successful in 23s
CI / eslint (pull_request) Successful in 1m26s
CI / test-build (pull_request) Successful in 1m50s
2025-04-02 11:54:06 +02:00
6b3a8a41fd
ci: remove debug job from CI workflow
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / eslint (pull_request) Successful in 29s
CI / test-build (pull_request) Successful in 35s
CI / prettier (pull_request) Successful in 1m0s
CI / Checkstyle Main (pull_request) Failing after 1m52s
2025-04-02 11:45:35 +02:00
06318a8b57
build(frontend): update ajv and ajv-formats versions
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 24s
CI / Checkstyle Main (pull_request) Failing after 33s
CI / eslint (pull_request) Failing after 1m38s
CI / test-build (pull_request) Successful in 1m51s
2025-04-02 11:42:53 +02:00
5d15f40a30
Merge pull request 'feat(blackjack): add split functionality to the game' (!116) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 48s
Reviewed-on: #116
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 09:41:18 +00:00
4c83419a31
Merge branch 'main' into task/CAS-50/add_rest_blackjack_logic_with_frontend_animations
All checks were successful
CI / Get Changed Files (pull_request) Successful in 8s
CI / eslint (pull_request) Successful in 21s
CI / test-build (pull_request) Successful in 28s
CI / prettier (pull_request) Successful in 42s
CI / Checkstyle Main (pull_request) Successful in 1m47s
2025-04-02 09:38:43 +00:00
f60ce73e65
build(frontend): add ajv-formats dependency to package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 24s
CI / Checkstyle Main (pull_request) Failing after 32s
CI / eslint (pull_request) Failing after 1m35s
CI / test-build (pull_request) Failing after 1m45s
2025-04-02 11:36:55 +02:00
bd82262049
ci: add debug step to CI workflow for better logging
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Failing after 24s
CI / test-build (pull_request) Failing after 27s
CI / prettier (pull_request) Successful in 1m3s
CI / Checkstyle Main (pull_request) Failing after 2m1s
2025-04-02 11:34:46 +02:00
6b4adfca0a
build(frontend): update angular compiler version to 19.2.4
Some checks failed
CI / Get Changed Files (pull_request) Successful in 10s
CI / eslint (pull_request) Failing after 35s
CI / prettier (pull_request) Successful in 39s
CI / test-build (pull_request) Failing after 42s
CI / Checkstyle Main (pull_request) Failing after 51s
2025-04-02 11:31:39 +02:00
1fc62af66d
build(frontend): update rxjs version to ~7.8.2
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / prettier (pull_request) Successful in 26s
CI / Checkstyle Main (pull_request) Failing after 36s
CI / eslint (pull_request) Failing after 1m26s
CI / test-build (pull_request) Failing after 1m24s
2025-04-02 11:31:08 +02:00
1916c04f4a
chore: update lint command in package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Failing after 20s
CI / test-build (pull_request) Failing after 25s
CI / prettier (pull_request) Successful in 1m0s
CI / Checkstyle Main (pull_request) Failing after 2m12s
2025-04-02 11:28:01 +02:00
575a6651d6
build(frontend): update Angular CLI version in package.json
Some checks failed
CI / Get Changed Files (pull_request) Successful in 9s
CI / eslint (pull_request) Failing after 33s
CI / prettier (pull_request) Successful in 34s
CI / Checkstyle Main (pull_request) Failing after 51s
CI / test-build (pull_request) Failing after 23s
2025-04-02 11:25:24 +02:00
942a47c1b2
build(frontend): update Angular dependencies to v19.0.0
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 23s
CI / Checkstyle Main (pull_request) Failing after 33s
CI / eslint (pull_request) Failing after 1m17s
CI / test-build (pull_request) Failing after 1m23s
2025-04-02 11:20:36 +02:00
ff98cab6ac
Merge pull request 'feat: Create api routes for lootboxes (CAS-43)' (!110) from feat/lootboxes into main
All checks were successful
Release / Release (push) Successful in 1m22s
Reviewed-on: #110
Reviewed-by: Jan K9f <jan@kjan.email>
Reviewed-by: lziemke <lea.z4@schule.bremen.de>
2025-04-02 09:18:54 +00:00
c64fccb236
Merge pull request 'chore(deps): update devdependencies (non-major)' (!114) from renovate/devdependencies-(non-major) into renovate-test
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 18s
CI / Checkstyle Main (pull_request) Failing after 31s
CI / eslint (pull_request) Successful in 59s
CI / test-build (pull_request) Failing after 58s
Reviewed-on: #114
2025-04-02 09:17:38 +00:00
210500783f
Merge pull request 'fix(deps): update dependencies (major and minor)' (!113) from renovate/major-dependencies-(major-and-minor) into renovate-test
Reviewed-on: #113
2025-04-02 09:16:18 +00:00
ec994616ee
Merge branch 'main' into feat/lootboxes
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 38s
2025-04-02 09:15:31 +00:00
Phan Huy Tran
948240ba1e fix: fix wrong reward getting returned, refactor to service
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 39s
2025-04-02 11:15:08 +02:00
Phan Huy Tran
b963595ab4 feat: manage balance
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m29s
2025-04-02 11:04:20 +02:00
aa613a95e3
fix(deps): update dependencies (major and minor)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / prettier (pull_request) Successful in 19s
CI / Checkstyle Main (pull_request) Failing after 31s
CI / eslint (pull_request) Successful in 55s
CI / test-build (pull_request) Failing after 52s
2025-04-02 09:02:01 +00:00
38f4617fb0
chore(deps): update devdependencies (non-major)
Some checks failed
CI / Get Changed Files (pull_request) Successful in 6s
CI / Checkstyle Main (pull_request) Has been skipped
CI / eslint (pull_request) Successful in 20s
CI / test-build (pull_request) Failing after 21s
CI / prettier (pull_request) Successful in 34s
2025-04-02 09:01:53 +00:00
626e28ab65
Merge pull request 'feat: add stand and get game features to blackjack game with animations' (!105) from task/CAS-50/add_rest_blackjack_logic_with_frontend_animations into main
All checks were successful
Release / Release (push) Successful in 1m1s
Reviewed-on: #105
Reviewed-by: Jan K9f <jan@kjan.email>
2025-04-02 08:28:43 +00:00
Phan Huy Tran
8a6bc95c92 feat: add route to get all lootboxes 2025-04-02 10:18:51 +02:00
Phan Huy Tran
e4bcd9d791 refactor: use many to many relation for lootboxes and rewards 2025-04-02 10:08:23 +02:00
Phan Huy Tran
084d478cd9 feat: create repositories
All checks were successful
CI / Get Changed Files (pull_request) Successful in 7s
CI / eslint (pull_request) Has been skipped
CI / prettier (pull_request) Has been skipped
CI / test-build (pull_request) Has been skipped
CI / Checkstyle Main (pull_request) Successful in 1m28s
2025-04-02 09:18:17 +02:00
Phan Huy Tran
1878ed8fe4 feat: create lootbox and rewards entity 2025-04-02 09:15:37 +02:00
24 changed files with 882 additions and 669 deletions

View file

@ -39,7 +39,7 @@ repositories {
} }
dependencies { dependencies {
implementation("com.stripe:stripe-java:20.136.0") implementation("com.stripe:stripe-java:29.0.0")
implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok") compileOnly("org.projectlombok:lombok")

View file

@ -0,0 +1,9 @@
GET http://localhost:8080/lootboxes
Authorization: Bearer {{token}}
Content-Type: application/json
###
POST http://localhost:8080/lootboxes/2
Authorization: Bearer {{token}}
Content-Type: application/json

View file

@ -1,10 +1,20 @@
package de.szut.casino; package de.szut.casino;
import de.szut.casino.lootboxes.LootBoxEntity;
import de.szut.casino.lootboxes.LootBoxRepository;
import de.szut.casino.lootboxes.RewardEntity;
import de.szut.casino.lootboxes.RewardRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@SpringBootApplication @SpringBootApplication
public class CasinoApplication { public class CasinoApplication {
@ -16,4 +26,65 @@ public class CasinoApplication {
public static RestTemplate restTemplate() { public static RestTemplate restTemplate() {
return new RestTemplate(); return new RestTemplate();
} }
@Bean
public CommandLineRunner initData(LootBoxRepository lootBoxRepository, RewardRepository rewardRepository) {
return _ -> {
if (lootBoxRepository.count() == 0) {
LootBoxEntity basicLootBox = new LootBoxEntity();
basicLootBox.setName("Basic LootBox");
basicLootBox.setPrice(new BigDecimal("2"));
basicLootBox.setRewards(new ArrayList<>()); // Initialize the list
LootBoxEntity premiumLootBox = new LootBoxEntity();
premiumLootBox.setName("Premium LootBox");
premiumLootBox.setPrice(new BigDecimal("5"));
premiumLootBox.setRewards(new ArrayList<>()); // Initialize the list
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
RewardEntity commonReward = new RewardEntity();
commonReward.setValue(new BigDecimal("0.50"));
commonReward.setProbability(new BigDecimal("0.7"));
RewardEntity rareReward = new RewardEntity();
rareReward.setValue(new BigDecimal("2.00"));
rareReward.setProbability(new BigDecimal("0.25"));
RewardEntity epicReward = new RewardEntity();
epicReward.setValue(new BigDecimal("5.00"));
epicReward.setProbability(new BigDecimal("0.5"));
RewardEntity premiumCommon = new RewardEntity();
premiumCommon.setValue(new BigDecimal("2.00"));
premiumCommon.setProbability(new BigDecimal("0.6"));
RewardEntity premiumRare = new RewardEntity();
premiumRare.setValue(new BigDecimal("5.00"));
premiumRare.setProbability(new BigDecimal("0.3"));
RewardEntity legendaryReward = new RewardEntity();
legendaryReward.setValue(new BigDecimal("15.00"));
legendaryReward.setProbability(new BigDecimal("0.10"));
rewardRepository.saveAll(Arrays.asList(
commonReward, rareReward, epicReward,
premiumCommon, premiumRare, legendaryReward
));
basicLootBox.getRewards().add(commonReward);
basicLootBox.getRewards().add(premiumRare);
premiumLootBox.getRewards().add(premiumCommon);
premiumLootBox.getRewards().add(premiumRare);
premiumLootBox.getRewards().add(legendaryReward);
lootBoxRepository.saveAll(Arrays.asList(basicLootBox, premiumLootBox));
System.out.println("Initial LootBoxes and rewards created successfully");
} else {
System.out.println("LootBoxes already exist, skipping initialization");
}
};
}
} }

View file

@ -52,10 +52,14 @@ public class DepositController {
SessionCreateParams params = SessionCreateParams.builder() SessionCreateParams params = SessionCreateParams.builder()
.addLineItem(SessionCreateParams.LineItem.builder() .addLineItem(SessionCreateParams.LineItem.builder()
.setAmount((long) amountDto.getAmount() * 100) .setPriceData(SessionCreateParams.LineItem.PriceData.builder()
.setCurrency("EUR") .setCurrency("EUR")
.setUnitAmount((long) amountDto.getAmount() * 100)
.setProductData(SessionCreateParams.LineItem.PriceData.ProductData.builder()
.setName("Einzahlung")
.build())
.build())
.setQuantity(1L) .setQuantity(1L)
.setName("Einzahlung")
.build()) .build())
.setSuccessUrl(frontendHost+"/home?success=true") .setSuccessUrl(frontendHost+"/home?success=true")
.setCancelUrl(frontendHost+"/home?success=false") .setCancelUrl(frontendHost+"/home?success=false")

View file

@ -40,7 +40,7 @@ public class TransactionService {
.build(); .build();
Session checkoutSession = Session.retrieve(sessionID, params, null); Session checkoutSession = Session.retrieve(sessionID, params, null);
if (!Objects.equals(checkoutSession.getPaymentStatus(), "paid")) { if (!"paid".equals(checkoutSession.getPaymentStatus())) {
return; return;
} }
@ -53,10 +53,12 @@ public class TransactionService {
transaction.setStatus(TransactionStatus.SUCCEEDED); transaction.setStatus(TransactionStatus.SUCCEEDED);
UserEntity user = transaction.getUser(); UserEntity user = transaction.getUser();
user.addBalance(checkoutSession.getAmountTotal()); Long amountTotal = checkoutSession.getAmountTotal();
if (amountTotal != null) {
user.addBalance(amountTotal);
}
userRepository.save(user); userRepository.save(user);
transactionRepository.save(transaction); transactionRepository.save(transaction);
} }
} }

View file

@ -51,12 +51,18 @@ public class WebhookController {
switch (event.getType()) { switch (event.getType()) {
case "checkout.session.completed": case "checkout.session.completed":
case "checkout.session.async_payment_succeeded": case "checkout.session.async_payment_succeeded":
Session session = (Session) event.getData().getObject(); EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
this.transactionService.fulfillCheckout(session.getId()); if (dataObjectDeserializer.getObject().isPresent()) {
Session session = (Session) dataObjectDeserializer.getObject().get();
this.transactionService.fulfillCheckout(session.getId());
} else {
logger.error("Failed to deserialize webhook event data");
}
break; break;
default: default:
// No action needed for other event types
break;
} }
return ResponseEntity.ok().body(null); return ResponseEntity.ok().body(null);

View file

@ -0,0 +1,58 @@
package de.szut.casino.lootboxes;
import de.szut.casino.user.UserEntity;
import de.szut.casino.user.UserRepository;
import de.szut.casino.user.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
public class LootBoxController {
private final LootBoxRepository lootBoxRepository;
private final UserService userService;
private final LootBoxService lootBoxService;
public LootBoxController(LootBoxRepository lootBoxRepository, UserRepository userRepository, 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, @RequestHeader("Authorization") String token) {
Optional<LootBoxEntity> optionalLootBox = lootBoxRepository.findById(id);
if (optionalLootBox.isEmpty()) {
return ResponseEntity.notFound().build();
}
LootBoxEntity lootBox = optionalLootBox.get();
Optional<UserEntity> optionalUser = userService.getCurrentUser(token);
if (optionalUser.isEmpty()) {
return ResponseEntity.notFound().build();
}
UserEntity user = optionalUser.get();
if (lootBoxService.hasSufficientBalance(user, lootBox.getPrice())) {
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "Insufficient balance");
return ResponseEntity.badRequest().body(errorResponse);
}
RewardEntity reward = lootBoxService.determineReward(lootBox);
lootBoxService.handleBalance(user, lootBox, reward);
return ResponseEntity.ok(reward);
}
}

View file

@ -0,0 +1,40 @@
package de.szut.casino.lootboxes;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import de.szut.casino.blackjack.CardEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLRestriction;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class LootBoxEntity {
@Id
@GeneratedValue
private Long id;
private String name;
@Column(precision = 19, scale = 2)
private BigDecimal price;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "lootbox_reward",
joinColumns = @JoinColumn(name = "lootbox_id"),
inverseJoinColumns = @JoinColumn(name = "reward_id")
)
private List<RewardEntity> rewards = new ArrayList<>();
}

View file

@ -0,0 +1,8 @@
package de.szut.casino.lootboxes;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
@Service
public interface LootBoxRepository extends JpaRepository<LootBoxEntity, Long> {
}

View file

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

View file

@ -0,0 +1,30 @@
package de.szut.casino.lootboxes;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Entity
public class RewardEntity {
@Id
@GeneratedValue
private Long id;
@Column(precision = 19, scale = 2)
private BigDecimal value;
@Column(precision = 5, scale = 2)
private BigDecimal probability;
@ManyToMany(mappedBy = "rewards")
@JsonBackReference
private List<LootBoxEntity> lootBoxes = new ArrayList<>();
}

View file

@ -0,0 +1,8 @@
package de.szut.casino.lootboxes;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
@Service
public interface RewardRepository extends JpaRepository<RewardEntity, Long> {
}

File diff suppressed because it is too large Load diff

View file

@ -9,39 +9,42 @@
"test": "bunx @angular/cli test", "test": "bunx @angular/cli test",
"format": "prettier --write \"src/**/*.{ts,html,css,scss}\"", "format": "prettier --write \"src/**/*.{ts,html,css,scss}\"",
"format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"", "format:check": "prettier --check \"src/**/*.{ts,html,css,scss}\"",
"lint": "ng lint" "lint": "bunx @angular/cli lint"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^18.2.0", "@angular/animations": "^19.0.0",
"@angular/cdk": "~18.2.14", "@angular/cdk": "~19.2.0",
"@angular/common": "^18.2.0", "@angular/common": "^19.0.0",
"@angular/compiler": "^18.2.0", "@angular/compiler": "^19.2.4",
"@angular/core": "^18.2.0", "@angular/core": "^19.0.0",
"@angular/forms": "^18.2.0", "@angular/forms": "^19.0.0",
"@angular/platform-browser": "^18.2.0", "@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^18.2.0", "@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^18.2.0", "@angular/router": "^19.0.0",
"@fortawesome/angular-fontawesome": "^1.0.0", "@fortawesome/angular-fontawesome": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@stripe/stripe-js": "^5.6.0", "@stripe/stripe-js": "^7.0.0",
"@tailwindcss/postcss": "^4.0.3", "@tailwindcss/postcss": "^4.0.3",
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
"countup.js": "^2.8.0",
"gsap": "^3.12.7", "gsap": "^3.12.7",
"keycloak-angular": "^16.0.1", "keycloak-angular": "^19.0.0",
"keycloak-js": "^25.0.5", "keycloak-js": "^26.0.0",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.2",
"tailwindcss": "^4.0.3", "tailwindcss": "^4.0.3",
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^18.2.2", "@angular-devkit/build-angular": "^19.0.0",
"@angular/cli": "^18.2.2", "@angular/cli": "^19.2.5",
"@angular/compiler-cli": "^18.2.0", "@angular/compiler-cli": "^19.0.0",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"angular-eslint": "19.2.1", "angular-eslint": "19.3.0",
"eslint": "^9.20.0", "eslint": "^9.20.0",
"jasmine-core": "~5.6.0", "jasmine-core": "~5.6.0",
"karma": "~6.4.0", "karma": "~6.4.0",
@ -50,7 +53,7 @@
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"typescript": "~5.5.0", "typescript": "~5.8.0",
"typescript-eslint": "8.26.1" "typescript-eslint": "8.29.0"
} }
} }

View file

@ -6,19 +6,6 @@
<app-dealer-hand [cards]="dealerCards()"></app-dealer-hand> <app-dealer-hand [cards]="dealerCards()"></app-dealer-hand>
<app-player-hand [cards]="playerCards()"></app-player-hand> <app-player-hand [cards]="playerCards()"></app-player-hand>
@if (isActionInProgress()) {
<div class="flex justify-center">
<div
class="card p-4 flex items-center gap-3 animate-pulse bg-deep-blue-light border border-deep-blue-light/50"
>
<div
class="w-5 h-5 rounded-full border-2 border-white border-t-transparent animate-spin"
></div>
<span>{{ currentAction() }}</span>
</div>
</div>
}
@if (gameInProgress()) { @if (gameInProgress()) {
<app-game-controls <app-game-controls
[playerCards]="playerCards()" [playerCards]="playerCards()"

View file

@ -48,7 +48,6 @@ export default class BlackjackComponent implements OnInit {
showGameResult = signal(false); showGameResult = signal(false);
isActionInProgress = signal(false); isActionInProgress = signal(false);
currentAction = signal<string>('');
showDebtDialog = signal(false); showDebtDialog = signal(false);
debtAmount = signal(0); debtAmount = signal(0);
@ -96,7 +95,6 @@ export default class BlackjackComponent implements OnInit {
onNewGame(bet: number): void { onNewGame(bet: number): void {
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.currentAction.set('Spiel wird gestartet...');
this.blackjackService.startGame(bet).subscribe({ this.blackjackService.startGame(bet).subscribe({
next: (game) => { next: (game) => {
@ -115,7 +113,6 @@ export default class BlackjackComponent implements OnInit {
if (!this.currentGameId() || this.isActionInProgress()) return; if (!this.currentGameId() || this.isActionInProgress()) return;
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.currentAction.set('Karte wird gezogen...');
this.blackjackService.hit(this.currentGameId()!).subscribe({ this.blackjackService.hit(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
@ -142,7 +139,6 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.currentAction.set('Dealer zieht Karten...');
this.blackjackService.stand(this.currentGameId()!).subscribe({ this.blackjackService.stand(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {
@ -167,7 +163,6 @@ export default class BlackjackComponent implements OnInit {
} }
this.isActionInProgress.set(true); this.isActionInProgress.set(true);
this.currentAction.set('Einsatz wird verdoppelt...');
this.blackjackService.doubleDown(this.currentGameId()!).subscribe({ this.blackjackService.doubleDown(this.currentGameId()!).subscribe({
next: (game) => { next: (game) => {

View file

@ -0,0 +1,76 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { CommonModule, CurrencyPipe } from '@angular/common';
import { CountUp } from 'countup.js';
@Component({
selector: 'app-animated-number',
standalone: true,
imports: [CommonModule, CurrencyPipe],
template: `
<span #numberElement>{{ formattedValue }}</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnimatedNumberComponent implements OnChanges, AfterViewInit {
@Input() value = 0;
@Input() duration = 1;
@Input() ease = 'power1.out';
@ViewChild('numberElement') numberElement!: ElementRef;
private countUp: CountUp | null = null;
private previousValue = 0;
formattedValue = '0,00 €';
ngAfterViewInit(): void {
this.initializeCountUp();
if (this.countUp && this.value !== 0) {
this.countUp.start(() => {
this.previousValue = this.value;
});
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['value']) {
if (this.countUp) {
const endVal = this.value;
this.countUp.update(endVal);
this.previousValue = endVal;
} else {
this.formattedValue = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(this.value);
}
}
}
private initializeCountUp(): void {
if (this.numberElement) {
this.countUp = new CountUp(this.numberElement.nativeElement, this.value, {
startVal: this.previousValue,
duration: this.duration,
easingFn: (t, b, c, d) => {
if (this.ease === 'power1.out') {
return c * (1 - Math.pow(1 - t / d, 1)) + b;
}
return c * (t / d) + b;
},
formattingFn: (value) => {
const formatted = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
this.formattedValue = formatted;
return formatted;
},
});
}
}
}

View file

@ -28,54 +28,27 @@ import { GameControlsService } from '@blackjack/services/game-controls.service';
(click)="hit.emit()" (click)="hit.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress" [disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
[class.opacity-50]="isActionInProgress"
> >
<span [class.invisible]="isActionInProgress">Ziehen</span> <span>Ziehen</span>
@if (isActionInProgress) {
<div class="absolute inset-0 flex items-center justify-center">
<div
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
></div>
</div>
}
</button> </button>
<button <button
(click)="stand.emit()" (click)="stand.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress" [disabled]="gameState !== GameState.IN_PROGRESS || isActionInProgress"
[class.opacity-50]="isActionInProgress"
> >
<span [class.invisible]="isActionInProgress">Halten</span> <span>Halten</span>
@if (isActionInProgress) {
<div class="absolute inset-0 flex items-center justify-center">
<div
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
></div>
</div>
}
</button> </button>
<button <button
(click)="doubleDown.emit()" (click)="doubleDown.emit()"
class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative" class="button-primary px-8 py-4 text-lg font-medium min-w-[120px] relative"
[disabled]=" [disabled]="!canDoubleDown || isActionInProgress"
gameState !== GameState.IN_PROGRESS || playerCards.length !== 2 || isActionInProgress
"
[class.opacity-50]="isActionInProgress"
> >
<span [class.invisible]="isActionInProgress">Verdoppeln</span> <span>Verdoppeln</span>
@if (isActionInProgress) {
<div class="absolute inset-0 flex items-center justify-center">
<div
class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"
></div>
</div>
}
</button> </button>
<button <button
(click)="leave.emit()" (click)="leave.emit()"
class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300" class="bg-accent-red hover:bg-accent-red/80 px-8 py-4 rounded text-lg font-medium min-w-[120px] transition-all duration-300"
[disabled]="isActionInProgress" [disabled]="isActionInProgress"
[class.opacity-50]="isActionInProgress"
> >
Abbrechen Abbrechen
</button> </button>
@ -97,4 +70,12 @@ export class GameControlsComponent {
protected readonly GameState = GameState; protected readonly GameState = GameState;
constructor(protected gameControlsService: GameControlsService) {} constructor(protected gameControlsService: GameControlsService) {}
get canDoubleDown(): boolean {
return (
this.gameState === GameState.IN_PROGRESS &&
this.playerCards.length === 2 &&
!this.isActionInProgress
);
}
} }

View file

@ -11,11 +11,12 @@ import {
import { CommonModule, CurrencyPipe } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { BettingService } from '@blackjack/services/betting.service'; import { BettingService } from '@blackjack/services/betting.service';
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-game-info', selector: 'app-game-info',
standalone: true, standalone: true,
imports: [CommonModule, CurrencyPipe, ReactiveFormsModule], imports: [CommonModule, CurrencyPipe, ReactiveFormsModule, AnimatedNumberComponent],
template: ` template: `
<div class="card p-4"> <div class="card p-4">
<h3 class="section-heading text-xl mb-4">Spiel Informationen</h3> <h3 class="section-heading text-xl mb-4">Spiel Informationen</h3>
@ -23,7 +24,7 @@ import { BettingService } from '@blackjack/services/betting.service';
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-text-secondary">Aktuelle Wette:</span> <span class="text-text-secondary">Aktuelle Wette:</span>
<span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'"> <span [class]="currentBet > 0 ? 'text-accent-red' : 'text-text-secondary'">
{{ currentBet | currency: 'EUR' }} <app-animated-number [value]="currentBet" [duration]="0.5"></app-animated-number>
</span> </span>
</div> </div>

View file

@ -2,11 +2,12 @@ import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter } from
import { CommonModule, CurrencyPipe } from '@angular/common'; import { CommonModule, CurrencyPipe } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { GameState } from '../../enum/gameState'; import { GameState } from '../../enum/gameState';
import { AnimatedNumberComponent } from '../animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-game-result', selector: 'app-game-result',
standalone: true, standalone: true,
imports: [CommonModule, CurrencyPipe], imports: [CommonModule, CurrencyPipe, AnimatedNumberComponent],
template: ` template: `
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;"> <div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
<div class="modal-card" [@cardAnimation]> <div class="modal-card" [@cardAnimation]>
@ -18,7 +19,9 @@ import { GameState } from '../../enum/gameState';
> >
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="text-text-secondary">Einsatz:</div> <div class="text-text-secondary">Einsatz:</div>
<div class="font-medium text-right">{{ amount | currency: 'EUR' }}</div> <div class="font-medium text-right">
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>
</div>
<div class="text-text-secondary"> <div class="text-text-secondary">
{{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }} {{ isDraw ? 'Zurückgegeben:' : isWin ? 'Gewonnen:' : 'Verloren:' }}
@ -31,9 +34,13 @@ import { GameState } from '../../enum/gameState';
'text-yellow-400': isDraw, 'text-yellow-400': isDraw,
}" }"
> >
{{ isLoss ? '-' : '+' }}{{ isWin ? amount * 2 : (amount | currency: 'EUR') }} {{ isLoss ? '-' : '+' }}
<app-animated-number
[value]="isWin ? amount * 2 : amount"
[duration]="0.5"
></app-animated-number>
<div *ngIf="isWin" class="text-xs text-text-secondary"> <div *ngIf="isWin" class="text-xs text-text-secondary">
(Einsatz {{ amount | currency: 'EUR' }} × 2) (Einsatz <app-animated-number [value]="amount" [duration]="0.5"></app-animated-number> × 2)
</div> </div>
</div> </div>
@ -41,7 +48,7 @@ import { GameState } from '../../enum/gameState';
Kontostand: Kontostand:
</div> </div>
<div class="font-medium text-right border-t border-text-secondary/20 pt-3"> <div class="font-medium text-right border-t border-text-secondary/20 pt-3">
{{ balance | currency: 'EUR' }} <app-animated-number [value]="balance" [duration]="0.5"></app-animated-number>
</div> </div>
</div> </div>
</div> </div>

View file

@ -13,7 +13,6 @@ export class UserService {
public currentUser$ = this.currentUserSubject.asObservable(); public currentUser$ = this.currentUserSubject.asObservable();
constructor() { constructor() {
// Initialize with current user data
this.getCurrentUser().subscribe(); this.getCurrentUser().subscribe();
} }

View file

@ -11,18 +11,19 @@ import {
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { interval, Subscription, takeWhile } from 'rxjs'; import { interval, Subscription, takeWhile } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-debt-dialog', selector: 'app-debt-dialog',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule, AnimatedNumberComponent],
template: ` template: `
<div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;"> <div *ngIf="visible" [@fadeInOut] class="modal-bg" style="z-index: 1000; position: fixed;">
<div class="modal-card" [@cardAnimation]> <div class="modal-card" [@cardAnimation]>
<h2 class="modal-heading text-accent-red">WARNUNG!</h2> <h2 class="modal-heading text-accent-red">WARNUNG!</h2>
<p class="py-2 text-text-secondary mb-4"> <p class="py-2 text-text-secondary mb-4">
Du hast nicht genug Geld für den Double Down. Du bist jetzt im Minus und schuldest uns Du hast nicht genug Geld für den Double Down. Du bist jetzt im Minus und schuldest uns
{{ amount | currency: 'EUR' }}. <app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>.
</p> </p>
<p class="py-2 text-accent-red mb-4 font-bold"> <p class="py-2 text-accent-red mb-4 font-bold">
Liefer das Geld sofort an den Dead Drop oder es wird unangenehme Konsequenzen geben! Liefer das Geld sofort an den Dead Drop oder es wird unangenehme Konsequenzen geben!
@ -32,7 +33,9 @@ import { interval, Subscription, takeWhile } from 'rxjs';
> >
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="text-text-secondary">Schulden:</div> <div class="text-text-secondary">Schulden:</div>
<div class="font-medium text-right text-accent-red">{{ amount | currency: 'EUR' }}</div> <div class="font-medium text-right text-accent-red">
<app-animated-number [value]="amount" [duration]="0.5"></app-animated-number>
</div>
</div> </div>
</div> </div>
<div class="text-center mb-6"> <div class="text-center mb-6">

View file

@ -19,9 +19,9 @@
class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300" class="text-white font-bold bg-deep-blue-contrast rounded-full px-4 py-2 text-sm hover:bg-deep-blue-contrast/80 hover:cursor-pointer hover:scale-105 transition-all active:scale-95 select-none duration-300"
routerLink="/home" routerLink="/home"
> >
<span [class]="balance() < 0 ? 'text-accent-red' : ''">{{ <span [class]="balance() < 0 ? 'text-accent-red' : ''">
balance() | currency: 'EUR' : 'symbol' : '1.2-2' <app-animated-number [value]="balance()" [duration]="0.5"></app-animated-number>
}}</span> </span>
</div> </div>
<button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button> <button (click)="logout()" class="button-primary px-4 py-1.5">Abmelden</button>
} }

View file

@ -11,12 +11,13 @@ import { KeycloakService } from 'keycloak-angular';
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { UserService } from '@service/user.service'; import { UserService } from '@service/user.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AnimatedNumberComponent } from '@blackjack/components/animated-number/animated-number.component';
@Component({ @Component({
selector: 'app-navbar', selector: 'app-navbar',
templateUrl: './navbar.component.html', templateUrl: './navbar.component.html',
standalone: true, standalone: true,
imports: [RouterModule, CurrencyPipe], imports: [RouterModule, CurrencyPipe, AnimatedNumberComponent],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NavbarComponent implements OnInit, OnDestroy { export class NavbarComponent implements OnInit, OnDestroy {