diff --git a/frontend/public/images/1-box.png b/frontend/public/images/1-box.png deleted file mode 100644 index b343dbd..0000000 Binary files a/frontend/public/images/1-box.png and /dev/null differ diff --git a/frontend/public/images/2-box.png b/frontend/public/images/2-box.png deleted file mode 100644 index 0ec4438..0000000 Binary files a/frontend/public/images/2-box.png and /dev/null differ diff --git a/frontend/public/images/3-box.png b/frontend/public/images/3-box.png deleted file mode 100644 index 87a8401..0000000 Binary files a/frontend/public/images/3-box.png and /dev/null differ diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index a207b91..ce4451c 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -26,15 +26,4 @@ export const routes: Routes = [ loadComponent: () => import('./feature/game/slots/slots.component'), canActivate: [authGuard], }, - { - path: 'game/lootboxes', - loadComponent: () => - import('./feature/lootboxes/lootbox-selection/lootbox-selection.component'), - canActivate: [authGuard], - }, - { - path: 'game/lootboxes/open/:id', - loadComponent: () => import('./feature/lootboxes/lootbox-opening/lootbox-opening.component'), - canActivate: [authGuard], - }, ]; diff --git a/frontend/src/app/feature/home/home.component.ts b/frontend/src/app/feature/home/home.component.ts index 5571d2b..be84450 100644 --- a/frontend/src/app/feature/home/home.component.ts +++ b/frontend/src/app/feature/home/home.component.ts @@ -82,7 +82,7 @@ export default class HomeComponent implements OnInit { id: '6', name: 'Lootboxen', image: '/lootbox.webp', - route: '/game/lootboxes', + route: '/game/lootbox', }, ]; 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 deleted file mode 100644 index 1272a49..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css +++ /dev/null @@ -1,320 +0,0 @@ -body { - background: linear-gradient(to bottom, #181c2a, #232c43); -} - -/* Color classes */ -.text-yellow-400 { - color: #facc15; -} -.text-purple-400 { - color: #a78bfa; -} -.text-blue-400 { - color: #60a5fa; -} - -.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.3); - border-radius: 50%; - border-top: 4px solid #fff; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; -} - -@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%; /* Back to center - we'll adjust the animation instead */ - transform: translate(-50%, -50%); /* Center precisely */ - 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 10s 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 */ - display: flex; - justify-content: center; /* Center the items container */ - align-items: center; -} - -.case-items { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 8px; - padding: 5px 0; - height: 100%; - width: 100%; - 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(4500px); - } - 100% { - transform: translateX(-37.5px); - } -} - -.case-item { - transition: all 0.2s ease; - padding: 2px; - animation: item-flash 0.3s ease-out forwards; - animation-play-state: paused; - width: 69px; - flex-shrink: 0; -} - -@keyframes item-flash { - 0% { - filter: brightness(1); - } - 50% { - filter: brightness(1.8); - } - 100% { - filter: brightness(1.2); - } -} - -.case-item-inner { - background: #232c43; - border: 2px solid #2d3748; - border-radius: 8px; - padding: 10px 5px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; -} - -.case-item-won { - z-index: 2; - 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 */ -} - -@keyframes highlight-winner { - 0% { - transform: scale(1); - filter: brightness(1); - } - 10% { - transform: scale(1.1); - filter: brightness(1.5); - } - 20% { - transform: scale(1.05); - filter: brightness(1.3); - } - 30% { - transform: scale(1.1); - filter: brightness(1.5); - } - 40% { - transform: scale(1.05); - filter: brightness(1.3); - } - 50% { - transform: scale(1.1); - filter: brightness(1.5); - } - 60% { - transform: scale(1.05); - filter: brightness(1.3); - } - 70% { - transform: scale(1.1); - filter: brightness(1.5); - } - 80% { - transform: scale(1.05); - filter: brightness(1.3); - } - 90% { - transform: scale(1.1); - filter: brightness(1.5); - } - 100% { - transform: scale(1.05); - filter: brightness(1.3); - } -} - -.amount { - font-size: 1rem; - font-weight: bold; - margin-bottom: 4px; -} - -.rarity { - 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 deleted file mode 100644 index 0da3ed8..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html +++ /dev/null @@ -1,150 +0,0 @@ - - -
-

Lootbox Öffnen

- -
-
-
- -
- {{ error }} -
- -
-
-
-
- -
- {{ lootbox.price | currency: 'EUR' }} -
-
- -
-

{{ lootbox.name }}

- -
- -
- -
-
-
Öffne Lootbox...
-
- -
-
-

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: - - {{ wonReward.value - (lootbox.price || 0) | currency: 'EUR' }} - -
-
-
-
-
-
-
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 deleted file mode 100644 index cd3ed1f..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { Component, ChangeDetectorRef } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; -import { LootboxService } from '../services/lootbox.service'; -import { LootBox, Reward } from 'app/model/LootBox'; -import { NavbarComponent } from '@shared/components/navbar/navbar.component'; - -@Component({ - selector: 'app-lootbox-opening', - standalone: true, - imports: [CommonModule, NavbarComponent], - templateUrl: './lootbox-opening.component.html', - styleUrls: ['./lootbox-opening.component.css'], -}) -export default class LootboxOpeningComponent { - lootbox: LootBox | null = null; - isLoading = true; - error = ''; - isOpening = false; - isOpen = false; - wonReward: Reward | null = null; - prizeList: Reward[] = []; - animationCompleted = false; - - constructor( - private route: ActivatedRoute, - private router: Router, - 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) => { - this.lootbox = lootboxes.find((box) => box.id === lootboxId) || null; - this.isLoading = false; - this.cdr.detectChanges(); - }, - error: () => { - this.error = 'Failed to load lootbox data'; - this.isLoading = false; - this.cdr.detectChanges(); - }, - }); - } - - openLootbox(): void { - if (!this.lootbox || this.isOpening) return; - - this.resetState(true); - - setTimeout(() => { - this.lootboxService.purchaseLootBox(this.lootbox!.id).subscribe({ - next: this.handleRewardSuccess.bind(this), - error: this.handleRewardError.bind(this), - }); - }, 100); - } - - 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; - - const prizeCount = 120; - const winningPosition = Math.floor(prizeCount / 2); - const possibleRewards = this.lootbox.rewards; - const items: Reward[] = []; - - for (let i = 0; i < prizeCount; i++) { - if (i === winningPosition) { - items.push({ ...wonReward }); - } else { - items.push(this.getWeightedRandomReward(possibleRewards)); - } - } - - this.prizeList = items; - - setTimeout(() => { - this.animationCompleted = true; - this.cdr.detectChanges(); - }, 10000); - } - - getWeightedRandomReward(rewards: Reward[]): Reward { - 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 { ...rewards[0] }; - } - - openAgain(): void { - this.resetState(); - this.openLootbox(); - } - - getBoxImage(id: number): string { - return `/images/${id}-box.png`; - } - - goBack(): void { - this.router.navigate(['/game/lootboxes']); - } - - isWonReward(reward: Reward): boolean { - if (!this.wonReward || !this.prizeList.length) return false; - - const middleIndex = Math.floor(this.prizeList.length / 2); - return this.prizeList.indexOf(reward) === middleIndex; - } - - 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.1) return 'text-epic'; - if (probability < 0.2) return 'text-rare'; - if (probability < 0.4) return 'text-uncommon'; - return 'text-common'; - } - - 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.css b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.css deleted file mode 100644 index 878220b..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.css +++ /dev/null @@ -1,28 +0,0 @@ -.loader { - border: 4px solid rgba(255, 255, 255, 0.3); - border-radius: 50%; - border-top: 4px solid #fff; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -.card { - transition: - transform 0.3s ease, - box-shadow 0.3s ease; -} - -.card:hover { - transform: translateY(-5px); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); -} 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 deleted file mode 100644 index bda9028..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.html +++ /dev/null @@ -1,65 +0,0 @@ - -
-

Lootboxen

- -
- isLoading: {{ isLoading }} | error: {{ error }} | lootboxes: {{ lootboxes.length }} -
- -
-
-
- -
- {{ error }} -
- -
-
-
- -
- {{ lootbox.price | currency: 'EUR' }} -
-
- -
-

{{ lootbox.name }}

- -
-

Mögliche Gewinne:

-
    -
  • - {{ - reward.value | currency: 'EUR' - }} - {{ formatProbability(reward.probability) }} -
  • -
-
- -
- -
-
- -
-
-

Fairness garantiert - Alle Ergebnisse werden transparent berechnet.

-
-
-
-
-
diff --git a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts deleted file mode 100644 index 217af16..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { NavbarComponent } from '@shared/components/navbar/navbar.component'; -import { LootboxService } from '../services/lootbox.service'; -import { LootBox } from 'app/model/LootBox'; -import { Router } from '@angular/router'; -import { timeout } from 'rxjs'; - -@Component({ - selector: 'app-lootbox-selection', - standalone: true, - imports: [CommonModule, NavbarComponent], - templateUrl: './lootbox-selection.component.html', - styleUrls: ['./lootbox-selection.component.css'], -}) -export default class LootboxSelectionComponent implements OnInit { - lootboxes: LootBox[] = []; - isLoading = true; - error = ''; - - // Fallback data in case the API call fails - fallbackLootboxes: LootBox[] = [ - { - id: 1, - name: 'Basic LootBox', - price: 2.0, - rewards: [ - { - id: 1, - value: 0.5, - probability: 0.7, - }, - { - id: 5, - value: 5.0, - probability: 0.3, - }, - ], - }, - { - id: 2, - name: 'Premium LootBox', - price: 5.0, - rewards: [ - { - id: 4, - value: 2.0, - probability: 0.6, - }, - { - id: 5, - value: 5.0, - probability: 0.3, - }, - { - id: 6, - value: 15.0, - probability: 0.1, - }, - ], - }, - { - id: 3, - name: 'Legendäre LootBox', - price: 15.0, - rewards: [ - { - id: 4, - value: 2.0, - probability: 0.6, - }, - { - id: 5, - value: 5.0, - probability: 0.3, - }, - { - id: 6, - value: 15.0, - probability: 0.1, - }, - ], - }, - ]; - - constructor( - private lootboxService: LootboxService, - private router: Router, - private cdr: ChangeDetectorRef - ) {} - - ngOnInit(): void { - this.loadLootboxes(); - } - - loadLootboxes(): void { - this.isLoading = true; - this.lootboxService - .getAllLootBoxes() - .pipe(timeout(5000)) - .subscribe({ - next: (data) => { - console.log('Received lootboxes:', data); - this.lootboxes = data; - this.isLoading = false; - this.cdr.detectChanges(); - }, - error: (err) => { - this.error = 'Konnte keine Verbindung zum Backend herstellen. Zeige Demo-Daten.'; - this.lootboxes = this.fallbackLootboxes; - this.isLoading = false; - this.cdr.detectChanges(); - console.error('Failed to load lootboxes:', err); - }, - }); - } - - getBoxImage(id: number): string { - return `/images/${id}-box.png`; - } - - openLootbox(lootboxId: number): void { - this.router.navigate(['/game/lootboxes/open', lootboxId]); - } - - getRarityClass(probability: number): string { - if (probability <= 0.1) { - return 'text-yellow-400'; // Legendary - } else if (probability <= 0.3) { - return 'text-purple-400'; // Rare - } else { - return 'text-blue-400'; // Common - } - } - - formatProbability(probability: number): string { - return (probability * 100).toFixed(0) + '%'; - } -} diff --git a/frontend/src/app/feature/lootboxes/services/lootbox.service.ts b/frontend/src/app/feature/lootboxes/services/lootbox.service.ts deleted file mode 100644 index b0fb58d..0000000 --- a/frontend/src/app/feature/lootboxes/services/lootbox.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, catchError } from 'rxjs'; -import { LootBox, Reward } from 'app/model/LootBox'; - -@Injectable({ - providedIn: 'root', -}) -export class LootboxService { - private http = inject(HttpClient); - - getAllLootBoxes(): Observable { - return this.http.get('/backend/lootboxes', { responseType: 'json' }).pipe( - catchError((error) => { - console.error('Get lootboxes error:', error); - throw error; - }) - ); - } - - purchaseLootBox(lootBoxId: number): Observable { - return this.http - .post(`/backend/lootboxes/${lootBoxId}`, {}, { responseType: 'json' }) - .pipe( - catchError((error) => { - console.error('Purchase lootbox error:', error); - throw error; - }) - ); - } -} diff --git a/frontend/src/app/model/LootBox.ts b/frontend/src/app/model/LootBox.ts deleted file mode 100644 index ebcf6e8..0000000 --- a/frontend/src/app/model/LootBox.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface Reward { - id: number; - value: number; - probability: number; -} - -export interface LootBox { - id: number; - name: string; - price: number; - rewards: Reward[]; -}