diff --git a/frontend/public/images/1-box.png b/frontend/public/images/1-box.png
new file mode 100644
index 0000000..b343dbd
Binary files /dev/null and b/frontend/public/images/1-box.png differ
diff --git a/frontend/public/images/2-box.png b/frontend/public/images/2-box.png
new file mode 100644
index 0000000..0ec4438
Binary files /dev/null and b/frontend/public/images/2-box.png differ
diff --git a/frontend/public/images/3-box.png b/frontend/public/images/3-box.png
new file mode 100644
index 0000000..87a8401
Binary files /dev/null and b/frontend/public/images/3-box.png differ
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index ce4451c..a207b91 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -26,4 +26,15 @@ 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 be84450..5571d2b 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/lootbox',
+ route: '/game/lootboxes',
},
];
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
new file mode 100644
index 0000000..1272a49
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css
@@ -0,0 +1,320 @@
+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
new file mode 100644
index 0000000..0da3ed8
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html
@@ -0,0 +1,150 @@
+
+
+
+
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
new file mode 100644
index 0000000..cd3ed1f
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts
@@ -0,0 +1,171 @@
+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
new file mode 100644
index 0000000..878220b
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.css
@@ -0,0 +1,28 @@
+.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
new file mode 100644
index 0000000..bda9028
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.html
@@ -0,0 +1,65 @@
+
+
+
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
new file mode 100644
index 0000000..217af16
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts
@@ -0,0 +1,139 @@
+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
new file mode 100644
index 0000000..b0fb58d
--- /dev/null
+++ b/frontend/src/app/feature/lootboxes/services/lootbox.service.ts
@@ -0,0 +1,31 @@
+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
new file mode 100644
index 0000000..ebcf6e8
--- /dev/null
+++ b/frontend/src/app/model/LootBox.ts
@@ -0,0 +1,12 @@
+export interface Reward {
+ id: number;
+ value: number;
+ probability: number;
+}
+
+export interface LootBox {
+ id: number;
+ name: string;
+ price: number;
+ rewards: Reward[];
+}