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 }}
-
-
-
-
-
-
-
-
-
-
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' }}
-
-
-
-
-
-
-
-
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[];
-}