From 45c5b19e41073750a46ae5fad908468a91bfe80e Mon Sep 17 00:00:00 2001 From: Jan K9f Date: Wed, 23 Apr 2025 13:47:36 +0200 Subject: [PATCH] style: clean up and reorganize lootbox component styles --- .../lootbox-opening.component.css | 218 ++++++++++++------ .../lootbox-opening.component.html | 81 ++++--- .../lootbox-opening.component.ts | 153 ++++++------ 3 files changed, 286 insertions(+), 166 deletions(-) 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 ef5b2b7..f99d259 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 @@ -2,88 +2,28 @@ body { background: linear-gradient(to bottom, #181c2a, #232c43); } -.case-strip-outer { - position: relative; - width: 900px; - max-width: 100vw; - height: 120px; - margin: 0 auto; - overflow: hidden; - background: #232c43; - border-radius: 16px; - border: 2px solid #2d3748; - box-shadow: 0 4px 32px 0 #000a; -} - -.case-strip-inner { - display: flex; - align-items: center; - height: 100%; - will-change: transform; -} - -.case-strip-reward { - width: 120px; - height: 100px; - margin: 0 6px; - background: #1a2233; - border-radius: 10px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 1.25rem; - font-weight: 600; - box-shadow: 0 2px 8px rgba(0,0,0,0.18); - border: 2px solid transparent; - transition: border-color 0.2s, color 0.2s; -} - +/* Color classes */ .text-yellow-400 { - border-color: #facc15; color: #facc15; } .text-purple-400 { - border-color: #a78bfa; color: #a78bfa; } .text-blue-400 { - border-color: #60a5fa; color: #60a5fa; } -.case-strip-marker { - position: absolute; - top: 0; - left: 50%; - transform: translateX(-50%); - width: 6px; - height: 100%; - background: linear-gradient(to bottom, #facc15 60%, #fff0 100%); - z-index: 2; - border-radius: 3px; - box-shadow: 0 0 16px #facc15, 0 0 2px #fff; - pointer-events: none; -} - -.open-btn { - background: linear-gradient(90deg, #facc15 0%, #a78bfa 100%); - color: #232c43; - font-size: 1.5rem; - font-weight: bold; - padding: 0.75rem 2.5rem; - border-radius: 999px; - border: none; - box-shadow: 0 2px 12px #0005; - transition: background 0.2s, color 0.2s, transform 0.1s; - cursor: pointer; -} -.open-btn:hover { - background: linear-gradient(90deg, #ffe066 0%, #c4b5fd 100%); - color: #181c2a; - transform: translateY(-2px) scale(1.04); +.border-yellow-400 { + border-color: #facc15; +} +.border-purple-400 { + border-color: #a78bfa; +} +.border-blue-400 { + border-color: #60a5fa; } +/* Loader animation */ .loader { border: 4px solid rgba(255, 255, 255, 0.2); border-radius: 50%; @@ -95,4 +35,142 @@ body { @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } +} + +/* Open button styling */ +.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); +} + +/* CSGO-style case opening display */ +.case-container { + position: relative; + width: 100%; + background: rgba(26, 31, 48, 0.6); + border-radius: 8px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + overflow: hidden; + margin-bottom: 20px; + display: flex; + justify-content: center; +} + +.case-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translateY(-50%); + width: 6px; + height: 100%; + background: #facc15; + box-shadow: 0 0 10px #facc15, 0 0 15px rgba(255, 255, 255, 0.5); + z-index: 3; + animation: indicator-pulse 1.5s ease-in-out 3s infinite alternate; +} + +@keyframes indicator-pulse { + 0% { + opacity: 0.6; + box-shadow: 0 0 10px #facc15, 0 0 15px rgba(255, 255, 255, 0.3); + } + 100% { + opacity: 1; + box-shadow: 0 0 15px #facc15, 0 0 20px rgba(255, 255, 255, 0.7); + } +} + +.case-items-container { + position: relative; + z-index: 1; + padding: 10px 5px; + margin: 0 auto; + width: 100%; + height: 150px; /* Fixed height for the horizontal row */ + overflow: hidden; /* Hide scrollbar */ +} + +.case-items { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 8px; + padding: 5px; + height: 100%; + width: 100%; + animation: slide-in 3s cubic-bezier(0.25, 1, 0.5, 1) forwards; + transform: translateX(120%); /* Initial position far to the right */ +} + +@keyframes slide-in { + 0% { + transform: translateX(120%); + } + 90% { + transform: translateX(-5%); /* Slight overshoot */ + } + 100% { + transform: translateX(0%); /* Final centered position */ + } +} + +.case-item { + transition: all 0.2s ease; + padding: 2px; +} + +.case-item-inner { + background: #232c43; + border: 2px solid #2d3748; + border-radius: 8px; + padding: 12px 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100px; + height: 100%; +} + +.case-item-won { + z-index: 2; + animation: highlight-winner 0.5s ease-out 3s forwards; +} + +.case-item-won .case-item-inner { + box-shadow: 0 0 15px rgba(255, 255, 255, 0.3); + background: linear-gradient(to right, #232c43, #2a3354, #232c43); +} + +@keyframes highlight-winner { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.15); + } + 75% { + transform: scale(1.1); + } + 100% { + transform: scale(1.05); + } +} + +.amount { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 4px; +} + +.rarity { + font-size: 0.75rem; + opacity: 0.7; } \ No newline at end of file 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 c72a20a..2c9834f 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 @@ -7,43 +7,72 @@

{{ lootbox.name }}

-
Preis: {{ lootbox.price | currency:'EUR' }}
+
Preis: {{ lootbox.price | currency:'EUR' }}
-
-
-
-
-
{{ reward.value | currency:'EUR' }}
-
{{ (reward.probability * 100) | number:'1.0-0' }}%
+ +
+

Mögliche Gewinne:

+
+
+
+ {{ reward.value | currency:'EUR' }}
-
-
-
-
- -
- -
Mögliche Gewinne:
-
-
- {{ reward.value | currency:'EUR' }} - ({{ (reward.probability * 100) | number:'1.0-0' }}%) +
Chance: {{ (reward.probability * 100) | number:'1.0-0' }}%
+ +
+ +
+ +
Öffne Lootbox...
-
-
Glückwunsch!
-
{{ wonReward.value | currency:'EUR' }}
-
wurde deinem Konto gutgeschrieben
-
- - + +
+ +
+
Dein Gewinn:
+
{{ wonReward?.value | currency:'EUR' }}
+
+ + + + +
+ +
+ + +
+
+
+
+
{{ reward.value | currency:'EUR' }}
+
{{ (reward.probability * 100) | number:'1.0-0' }}%
+
+
+
+
+
+ + +
+ +
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 c31ce42..6e2eedd 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 @@ -25,25 +25,12 @@ export default class LootboxOpeningComponent { lootbox: LootBox | null = null; isLoading = true; error = ''; - + // UI State isOpening = false; + isOpen = false; wonReward: Reward | null = null; - strip: Reward[] = []; - stripTranslateX = 0; - stripTransition = 'none'; - - // Config - readonly visibleCount = 7; - readonly rewardWidth = 120; - readonly stripLength = 200; - readonly ticks = 60; - readonly minTickMs = 30; - readonly maxTickMs = 180; - private tickIndex = 0; - private stopIndex = 0; - private center = Math.floor(this.visibleCount / 2); - private animationTimeout: any; + prizeList: Reward[] = []; constructor( private route: ActivatedRoute, @@ -75,68 +62,84 @@ export default class LootboxOpeningComponent { openLootbox() { if (!this.lootbox || this.isOpening) return; this.isOpening = true; + this.isOpen = false; this.wonReward = null; - this.stripTransition = 'none'; - this.stripTranslateX = 0; + this.prizeList = []; // Clear previous prizes this.cdr.detectChanges(); - this.lootboxService.purchaseLootBox(this.lootbox.id).subscribe({ - next: (reward) => this.startSlidingAnimation(reward), - error: () => { - const rewards = this.lootbox!.rewards; - const fallback = rewards[Math.floor(Math.random() * rewards.length)]; - this.startSlidingAnimation(fallback); + + // Short delay to ensure animation plays from the beginning + 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(); + } + }); + }, 100); + } + + generateCasePrizes(wonReward: Reward) { + if (!this.lootbox) return; + + // Create a case opening display with fewer prizes to fit on screen without scrolling + const prizeCount = 9; // Total number of prizes to show (odd number to have center prize) + const winningPosition = Math.floor(prizeCount / 2); // Position of the winning prize (middle) + + // Get possible rewards from the lootbox + const possibleRewards = this.lootbox.rewards; + + // Generate an array of random rewards + let items: Reward[] = []; + for (let i = 0; i < prizeCount; i++) { + // Special handling for the winning position + if (i === winningPosition) { + 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); } - }); - } - - private startSlidingAnimation(won: Reward) { - // Fill the strip with shuffled rewards, repeat as needed - const rewards = this.lootbox!.rewards; - let strip: Reward[] = []; - while (strip.length < this.stripLength) { - strip = strip.concat(shuffle(rewards)); } - // Place the won reward at the final center position - this.center = Math.floor(this.visibleCount / 2); - this.stopIndex = this.ticks + this.center; - strip[this.stopIndex] = won; - this.strip = strip; - this.stripTransition = 'none'; - this.stripTranslateX = 0; - this.tickIndex = 0; - this.cdr.detectChanges(); - this.stepSliding(); + + this.prizeList = items; } - - private stepSliding() { - if (this.tickIndex > this.ticks) { - // Animation done - this.stripTransition = 'none'; - this.stripTranslateX = -((this.stopIndex - this.center) * this.rewardWidth); - this.isOpening = false; - this.wonReward = this.strip[this.stopIndex]; - this.cdr.detectChanges(); - return; + + 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 + } } - // Ease-out: slow down at the end - const progress = this.tickIndex / this.ticks; - const eased = 1 - Math.pow(1 - progress, 2.5); - const currentIndex = Math.round(eased * (this.stopIndex - this.center)); - this.stripTransition = 'transform 0.12s cubic-bezier(0.4,0.7,0.5,1)'; - this.stripTranslateX = -(currentIndex * this.rewardWidth); - this.cdr.detectChanges(); - // Calculate next tick interval - const tickMs = this.minTickMs + (this.maxTickMs - this.minTickMs) * eased; - this.tickIndex++; - this.animationTimeout = setTimeout(() => this.stepSliding(), tickMs); + + // Fallback, should never reach here + return { ...rewards[0] }; } openAgain() { this.isOpening = false; + this.isOpen = false; this.wonReward = null; - this.strip = []; - this.stripTranslateX = 0; - this.stripTransition = 'none'; + this.prizeList = []; this.cdr.detectChanges(); } @@ -149,8 +152,18 @@ export default class LootboxOpeningComponent { } getRarityClass(prob: number): string { - if (prob <= 0.1) return 'text-yellow-400'; - if (prob <= 0.3) return 'text-purple-400'; - return 'text-blue-400'; + if (prob <= 0.1) return 'text-yellow-400 border-yellow-400'; + if (prob <= 0.3) return 'text-purple-400 border-purple-400'; + return 'text-blue-400 border-blue-400'; + } + + 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; } } \ No newline at end of file