diff --git a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css
index 28fd570..c4f70f7 100644
--- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css
+++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css
@@ -25,13 +25,14 @@ body {
/* Loader animation */
.loader {
- border: 4px solid rgba(255, 255, 255, 0.2);
+ border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
- border-top: 4px solid #facc15;
+ border-top: 4px solid #fff;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
+
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -115,23 +116,20 @@ body {
justify-content: center;
align-items: center;
gap: 8px;
- padding: 5px 0px; /* Remove horizontal padding */
+ padding: 5px 0;
height: 100%;
width: 100%;
- animation: slide-in 10s cubic-bezier(0.05, 0.82, 0.17, 1) forwards;
- transform: translateX(3000%); /* Extremely far initial position for 200 items */
- position: relative; /* Ensure positioning context */
+ animation: slide-in 10s cubic-bezier(0.33, 0.9, 0.3, 1) forwards;
+ transform: translateX(4500px);
+ position: relative;
}
@keyframes slide-in {
0% {
- transform: translateX(2000%);
- }
- 90% {
- transform: translateX(-16%); /* Single overshoot for dramatic effect */
+ transform: translateX(4500px);
}
100% {
- transform: translateX(-8%); /* Centered position */
+ transform: translateX(-37.5px);
}
}
@@ -140,6 +138,8 @@ body {
padding: 2px;
animation: item-flash 0.3s ease-out forwards;
animation-play-state: paused;
+ width: 69px;
+ flex-shrink: 0;
}
@keyframes item-flash {
@@ -163,7 +163,6 @@ body {
flex-direction: column;
align-items: center;
justify-content: center;
- width: 65px;
height: 100%;
}
@@ -172,6 +171,16 @@ body {
animation: highlight-winner 1s ease-out 10s forwards;
}
+.winning-prize {
+ border: 2px solid transparent;
+ transition: all 0.5s ease;
+}
+
+.winning-prize.highlight {
+ border-color: #fff !important;
+ box-shadow: 0 0 8px rgba(255, 255, 255, 0.6) !important;
+}
+
/* Specific ID for the winning item to ensure it's visible */
#winning-item {
z-index: 5; /* Higher than indicator */
@@ -183,44 +192,44 @@ body {
filter: brightness(1);
}
10% {
- transform: scale(1.3);
- filter: brightness(2);
+ transform: scale(1.1);
+ filter: brightness(1.5);
}
20% {
- transform: scale(1.2);
- filter: brightness(1.8);
+ transform: scale(1.05);
+ filter: brightness(1.3);
}
30% {
- transform: scale(1.25);
- filter: brightness(2);
+ transform: scale(1.1);
+ filter: brightness(1.5);
}
40% {
- transform: scale(1.2);
- filter: brightness(1.8);
+ transform: scale(1.05);
+ filter: brightness(1.3);
}
50% {
- transform: scale(1.25);
- filter: brightness(2);
+ transform: scale(1.1);
+ filter: brightness(1.5);
}
60% {
- transform: scale(1.2);
- filter: brightness(1.8);
+ transform: scale(1.05);
+ filter: brightness(1.3);
}
70% {
- transform: scale(1.25);
- filter: brightness(2);
+ transform: scale(1.1);
+ filter: brightness(1.5);
}
80% {
- transform: scale(1.2);
- filter: brightness(1.8);
+ transform: scale(1.05);
+ filter: brightness(1.3);
}
90% {
- transform: scale(1.25);
- filter: brightness(2);
+ transform: scale(1.1);
+ filter: brightness(1.5);
}
100% {
- transform: scale(1.2);
- filter: brightness(1.5);
+ transform: scale(1.05);
+ filter: brightness(1.3);
}
}
@@ -234,3 +243,76 @@ body {
font-size: 0.75rem;
opacity: 0.7;
}
+
+/* Prize animation */
+.prize-reel {
+ animation: slide-prizes 10s cubic-bezier(0.05, 0.82, 0.17, 1) forwards;
+}
+
+@keyframes slide-prizes {
+ 0% {
+ transform: translateX(800px);
+ }
+ 85% {
+ transform: translateX(-120px); /* Small overshoot */
+ }
+ 92% {
+ transform: translateX(-90px); /* Bounce back */
+ }
+ 100% {
+ transform: translateX(-100px); /* Final position centered */
+ }
+}
+
+.animate-item-flash {
+ animation: item-flash 0.5s ease-out alternate infinite;
+}
+
+.highlight {
+ animation: highlight-winner 1s ease-out forwards;
+}
+
+/* Reward rarity classes */
+.text-common {
+ color: #ffffff;
+}
+
+.text-uncommon {
+ color: #4ade80;
+}
+
+.text-rare {
+ color: #60a5fa;
+}
+
+.text-epic {
+ color: #a78bfa;
+}
+
+.text-legendary {
+ color: #facc15;
+}
+
+.text-mythic {
+ color: #f87171;
+}
+
+.text-emerald {
+ color: #10b981;
+}
+
+.text-accent-red {
+ color: #ef4444;
+}
+
+.animation-fade {
+ opacity: 0;
+ transform: translateY(10px);
+ transition: opacity 0.5s ease-out, transform 0.5s ease-out;
+ transition-delay: 0.5s;
+}
+
+.animation-fade.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
diff --git a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html
index c85802d..531d1a0 100644
--- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html
+++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html
@@ -1,136 +1,133 @@
-
-
-
-
Lade Lootbox...
+
+
+
Lootbox Öffnen
+
+
-
-
- {{ lootbox.name }}
- Preis: {{ lootbox.price | currency: 'EUR' }}
-
-
-
-
Mögliche Gewinne:
-
-
-
- {{ reward.value | currency: 'EUR' }}
-
-
- Chance: {{ reward.probability * 100 | number: '1.0-0' }}%
+
+
+ {{ error }}
+
+
+
+
+
+
+
![]()
+
+ {{ lootbox.price | currency:'EUR' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Dein Gewinn:
-
- {{ wonReward?.value | currency: 'EUR' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ reward.value | currency: 'EUR' }}
-
{{ reward.probability * 100 | number: '1.0-0' }}%
+
+
+
{{ lootbox.name }}
+
+
+
+
+
+
+
+
+
+
Dein Gewinn:
+
+ {{ wonReward?.value | currency: 'EUR' }}
-
-
-
+
+
+
+
+
+
+
+
+
+ {{ reward.value | currency: 'EUR' }}
+
+
+ {{ reward.probability * 100 | number: '1.0-0' }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Fairness garantiert - Alle Ergebnisse werden transparent berechnet.
-
-
-
-
-
+
+
+
+
+
Mögliche Gewinne:
+
+ -
+ {{ reward.value | currency:'EUR' }}
+ {{ reward.probability * 100 | number: '1.0-0' }}%
+
+
+
+
+
Gewinn-Details:
+
+
+ Kosten:
+ {{ lootbox.price | currency: 'EUR' }}
+
+
+ Gewinn:
+ {{ wonReward.value | currency: 'EUR' }}
+
+
+ Profit:
+ (lootbox.price || 0) ? 'text-emerald' : 'text-accent-red'">
+ {{ (wonReward.value - (lootbox.price || 0)) | currency: 'EUR' }}
+
+
+
+
-
-
+
+
\ No newline at end of file
diff --git a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts
index 39a23a9..ca7c3ca 100644
--- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts
+++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts
@@ -5,15 +5,6 @@ import { LootboxService } from '../services/lootbox.service';
import { LootBox, Reward } from 'app/model/LootBox';
import { NavbarComponent } from '@shared/components/navbar/navbar.component';
-function shuffle
(array: T[]): T[] {
- const arr = array.slice();
- for (let i = arr.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [arr[i], arr[j]] = [arr[j], arr[i]];
- }
- return arr;
-}
-
@Component({
selector: 'app-lootbox-opening',
standalone: true,
@@ -25,12 +16,11 @@ export default class LootboxOpeningComponent {
lootbox: LootBox | null = null;
isLoading = true;
error = '';
-
- // UI State
isOpening = false;
isOpen = false;
wonReward: Reward | null = null;
prizeList: Reward[] = [];
+ animationCompleted = false;
constructor(
private route: ActivatedRoute,
@@ -38,12 +28,17 @@ export default class LootboxOpeningComponent {
private lootboxService: LootboxService,
private cdr: ChangeDetectorRef
) {
+ this.loadLootbox();
+ }
+
+ private loadLootbox(): void {
const idParam = this.route.snapshot.paramMap.get('id');
if (!idParam) {
this.error = 'Invalid lootbox ID';
this.isLoading = false;
return;
}
+
const lootboxId = parseInt(idParam, 10);
this.lootboxService.getAllLootBoxes().subscribe({
next: (lootboxes) => {
@@ -59,88 +54,86 @@ export default class LootboxOpeningComponent {
});
}
- openLootbox() {
+ openLootbox(): void {
if (!this.lootbox || this.isOpening) return;
- this.isOpening = true;
- this.isOpen = false;
- this.wonReward = null;
- this.prizeList = []; // Clear previous prizes
- this.cdr.detectChanges();
-
- // Short delay to ensure animation plays from the beginning
+
+ this.resetState(true);
+
setTimeout(() => {
this.lootboxService.purchaseLootBox(this.lootbox!.id).subscribe({
- next: (reward) => {
- this.wonReward = reward;
- this.generateCasePrizes(reward);
- this.isOpening = false;
- this.isOpen = true;
- this.cdr.detectChanges();
- },
- error: () => {
- // Fallback if API fails
- const rewards = this.lootbox!.rewards;
- const fallback = rewards[Math.floor(Math.random() * rewards.length)];
- this.wonReward = fallback;
- this.generateCasePrizes(fallback);
- this.isOpening = false;
- this.isOpen = true;
- this.cdr.detectChanges();
- },
+ next: this.handleRewardSuccess.bind(this),
+ error: this.handleRewardError.bind(this),
});
}, 100);
}
- generateCasePrizes(wonReward: Reward) {
+ private handleRewardSuccess(reward: Reward): void {
+ this.wonReward = reward;
+ this.generateCasePrizes(reward);
+ this.isOpening = false;
+ this.isOpen = true;
+ this.cdr.detectChanges();
+ }
+
+ private handleRewardError(): void {
+ if (!this.lootbox) return;
+
+ const rewards = this.lootbox.rewards;
+ const fallback = rewards[Math.floor(Math.random() * rewards.length)];
+ this.handleRewardSuccess(fallback);
+ }
+
+ private resetState(isOpening = false): void {
+ this.isOpening = isOpening;
+ this.isOpen = false;
+ this.wonReward = null;
+ this.prizeList = [];
+ this.animationCompleted = false;
+ this.cdr.detectChanges();
+ }
+
+ generateCasePrizes(wonReward: Reward): void {
if (!this.lootbox) return;
- // Create a case opening display with an extremely large number of prizes for a massive scrolling animation
- const prizeCount = 200; // Set to 200 for an extremely long scrolling animation
- const winningPosition = Math.floor(prizeCount / 2); // Position of the winning prize (middle)
-
- // Get possible rewards from the lootbox
+ const prizeCount = 120;
+ const winningPosition = Math.floor(prizeCount / 2);
const possibleRewards = this.lootbox.rewards;
-
- // Generate an array of random rewards
- let items: Reward[] = [];
+ const items: Reward[] = [];
+
for (let i = 0; i < prizeCount; i++) {
- // Special handling for the winning position
if (i === winningPosition) {
- items.push(wonReward);
+ items.push({...wonReward});
} else {
- // For all other positions, choose a random reward
- // Weight rarer items to appear less frequently
- const randomReward = this.getWeightedRandomReward(possibleRewards);
- items.push(randomReward);
+ items.push(this.getWeightedRandomReward(possibleRewards));
}
}
this.prizeList = items;
+
+ setTimeout(() => {
+ this.animationCompleted = true;
+ this.cdr.detectChanges();
+ }, 10000);
}
getWeightedRandomReward(rewards: Reward[]): Reward {
- // Create a weighted distribution based on probabilities
const totalProbability = rewards.reduce((sum, reward) => sum + reward.probability, 0);
const randomValue = Math.random() * totalProbability;
-
let cumulativeProbability = 0;
+
for (const reward of rewards) {
cumulativeProbability += reward.probability;
if (randomValue <= cumulativeProbability) {
- return { ...reward }; // Return a copy of the reward
+ return { ...reward };
}
}
-
- // Fallback, should never reach here
+
return { ...rewards[0] };
}
- openAgain() {
- this.isOpening = false;
- this.isOpen = false;
- this.wonReward = null;
- this.prizeList = [];
- this.cdr.detectChanges();
+ openAgain(): void {
+ this.resetState();
+ this.openLootbox();
}
getBoxImage(id: number): string {
@@ -152,25 +145,28 @@ export default class LootboxOpeningComponent {
}
isWonReward(reward: Reward): boolean {
- if (!this.wonReward) {
- return false;
- }
-
- return (
- reward.id === this.wonReward.id &&
- reward.value === this.wonReward.value &&
- reward.probability === this.wonReward.probability
- );
+ if (!this.wonReward || !this.prizeList.length) return false;
+
+ const middleIndex = Math.floor(this.prizeList.length / 2);
+ return this.prizeList.indexOf(reward) === middleIndex;
}
-
- // Calculate the center position for better alignment
- getCenterOffset(): string {
- return '0px'; // No additional offset - using animation transform instead
+
+ getRewardRarityClass(reward: Reward): string {
+ if (!reward) return 'text-common';
+
+ const probability = reward.probability;
+
+ if (probability < 0.01) return 'text-mythic';
+ if (probability < 0.05) return 'text-legendary';
+ if (probability < 0.10) return 'text-epic';
+ if (probability < 0.20) return 'text-rare';
+ if (probability < 0.40) return 'text-uncommon';
+ return 'text-common';
}
-
- // Check if item is at center position (100th item)
- isCenterItem(index: number): boolean {
- return index === Math.floor(200 / 2); // Center item at index 100 (0-indexed)
+
+ getRewardClass(): string {
+ if (!this.wonReward || !this.lootbox) return '';
+ return this.wonReward.value > (this.lootbox.price || 0) ? 'text-emerald' : 'text-accent-red';
}
}
diff --git a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.html b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.html
index 070e084..a4293fd 100644
--- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.html
+++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.html
@@ -2,7 +2,7 @@
Lootboxen
-
isLoading: {{ isLoading }} | error: {{ error }} | lootboxes: {{ lootboxes?.length }}
+
isLoading: {{ isLoading }} | error: {{ error }} | lootboxes: {{ lootboxes.length }}