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 bba628f..169e387 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -26,4 +26,9 @@ export const routes: Routes = [ 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 eb12454..7320cd0 100644 --- a/frontend/src/app/feature/home/home.component.ts +++ b/frontend/src/app/feature/home/home.component.ts @@ -68,7 +68,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/README.md b/frontend/src/app/feature/lootboxes/README.md deleted file mode 100644 index 2aecc6b..0000000 --- a/frontend/src/app/feature/lootboxes/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Lootboxes Feature - -Diese Funktion ermöglicht es Spielern, Lootboxen zu kaufen und zu öffnen. - -## Komponenten - -- **LootboxSelection**: Zeigt alle verfügbaren Lootboxen an und ermöglicht Filterung nach Kategorien. -- (Weitere Komponenten werden implementiert) - -## Implementierung - -### Lootbox-Kategorien -- Gewöhnlich (Common): Niedrigere Preise, höhere Gewinnchance -- Selten (Rare): Mittlere Preise, mittlere Gewinnchance -- Legendär (Legendary): Hohe Preise, niedrige Gewinnchance - -### Routen -- `/lootboxes`: Hauptseite mit Lootbox-Auswahl -- `/lootboxes/open/:id`: Öffnen einer bestimmten Lootbox -- `/lootboxes/fairness`: Informationen zur Fairness-Garantie - -## Hinweise zur Implementierung - -1. **Bilder**: Stelle sicher, dass die korrekten Bilder unter `/assets/images/lootboxes/` vorhanden sind. -2. **Styling**: Das Styling ist an Counter-Strike-Lootboxen angelehnt. -3. **Authentifizierung**: Alle Lootbox-Routen sind durch den Auth-Guard geschützt. - -## Lootbox-Objekt-Interface - -```typescript -interface Lootbox { - id: string; - name: string; - category: 'common' | 'rare' | 'legendary'; - price: number; - image: string; - chance: number; - maxPrize: number; -} -``` \ No newline at end of file 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..ef5b2b7 --- /dev/null +++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.css @@ -0,0 +1,98 @@ +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; +} + +.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); +} + +.loader { + border: 4px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + border-top: 4px solid #facc15; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ 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 new file mode 100644 index 0000000..c72a20a --- /dev/null +++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.html @@ -0,0 +1,50 @@ + +
+
+
+
Lade Lootbox...
+
+ + +

{{ lootbox.name }}

+
Preis: {{ lootbox.price | currency:'EUR' }}
+ +
+
+
+
+
{{ reward.value | currency:'EUR' }}
+
{{ (reward.probability * 100) | number:'1.0-0' }}%
+
+
+
+
+
+ +
+ +
Mögliche Gewinne:
+
+
+ {{ reward.value | currency:'EUR' }} + ({{ (reward.probability * 100) | number:'1.0-0' }}%) +
+
+
+ +
+
+
Öffne Lootbox...
+
+ +
+
Glückwunsch!
+
{{ wonReward.value | currency:'EUR' }}
+
wurde deinem Konto gutgeschrieben
+
+ + +
+
+
+
\ 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 new file mode 100644 index 0000000..c31ce42 --- /dev/null +++ b/frontend/src/app/feature/lootboxes/lootbox-opening/lootbox-opening.component.ts @@ -0,0 +1,156 @@ +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'; + +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, + imports: [CommonModule, NavbarComponent], + templateUrl: './lootbox-opening.component.html', + styleUrls: ['./lootbox-opening.component.css'] +}) +export default class LootboxOpeningComponent { + lootbox: LootBox | null = null; + isLoading = true; + error = ''; + + // UI State + isOpening = 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; + + constructor( + private route: ActivatedRoute, + private router: Router, + private lootboxService: LootboxService, + private cdr: ChangeDetectorRef + ) { + 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() { + if (!this.lootbox || this.isOpening) return; + this.isOpening = true; + this.wonReward = null; + this.stripTransition = 'none'; + this.stripTranslateX = 0; + 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); + } + }); + } + + 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(); + } + + 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; + } + // 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); + } + + openAgain() { + this.isOpening = false; + this.wonReward = null; + this.strip = []; + this.stripTranslateX = 0; + this.stripTransition = 'none'; + this.cdr.detectChanges(); + } + + getBoxImage(id: number): string { + return `/images/${id}-box.png`; + } + + goBack(): void { + this.router.navigate(['/game/lootboxes']); + } + + getRarityClass(prob: number): string { + if (prob <= 0.1) return 'text-yellow-400'; + if (prob <= 0.3) return 'text-purple-400'; + return 'text-blue-400'; + } +} \ No newline at end of file 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..bbeb8b0 --- /dev/null +++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.css @@ -0,0 +1,22 @@ +.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); +} \ No newline at end of file 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 3b6d80c..070e084 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 @@ -1,53 +1,51 @@ -
-
-

Lootboxen

-
- - -
-
- -
-

Alle unsere Lootboxen unterliegen einem verifizierbaren Fairness-System. - Die Gewinnchancen sind transparent und werden von unabhängigen Prüfern bestätigt.

- Mehr zur Fairness-Garantie + +
+

Lootboxen

+ +
isLoading: {{ isLoading }} | error: {{ error }} | lootboxes: {{ lootboxes?.length }}
+ +
+
- -
-
-
-
- -
{{ lootbox.category | titlecase }}
+ +
+ {{ error }} +
+ +
+
+
+ +
+ {{ lootbox.price | currency:'EUR' }}
-
-

{{ lootbox.name }}

-
-
- Preis: - {{ lootbox.price }}€ -
-
- Wahrscheinlichkeit: - {{ lootbox.chance }}% -
-
- Höchster Preis: - {{ lootbox.maxPrize }}€ -
-
- +
+ +
+

{{ 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.scss b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.scss deleted file mode 100644 index 879dccc..0000000 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.scss +++ /dev/null @@ -1,188 +0,0 @@ - -.lootbox-container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; - background-color: #1a1a1a; - color: #fff; - font-family: 'Inter', sans-serif; -} - -.lootbox-header { - text-align: center; - margin-bottom: 2rem; - - h1 { - font-size: 2.5rem; - font-weight: 700; - margin-bottom: 1rem; - color: #f8f8f8; - text-transform: uppercase; - letter-spacing: 2px; - } -} - -.category-filters { - display: flex; - justify-content: center; - gap: 1rem; - margin: 1.5rem 0; - - .category-btn { - background-color: #2a2a2a; - border: none; - color: #aaa; - padding: 0.5rem 1.5rem; - border-radius: 4px; - cursor: pointer; - transition: all 0.2s ease; - font-weight: 500; - - &:hover { - background-color: #3a3a3a; - color: #fff; - } - - &.active { - background-color: #4a4a4a; - color: #fff; - box-shadow: 0 0 0 2px rgba(#ffcc00, 0.5); - } - } -} - -.fairness-info { - background-color: rgba(#ffcc00, 0.1); - border-left: 4px solid #ffcc00; - padding: 1rem; - margin-bottom: 2rem; - border-radius: 4px; - - p { - margin: 0 0 0.5rem 0; - color: #ddd; - font-size: 0.9rem; - } - - .info-link { - color: #ffcc00; - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - - &:hover { - text-decoration: underline; - } - } -} - -.lootbox-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 2rem; -} - -.lootbox-card { - background: linear-gradient(135deg, #2a2a2a 0%, #1c1c1c 100%); - border-radius: 8px; - overflow: hidden; - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); - transition: transform 0.3s ease, box-shadow 0.3s ease; - - &:hover { - transform: translateY(-5px); - box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4); - } -} - -.lootbox-image-container { - position: relative; - padding-bottom: 75%; - overflow: hidden; - - .lootbox-image { - position: absolute; - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; - } - - .lootbox-category { - position: absolute; - top: 10px; - right: 10px; - padding: 5px 10px; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 600; - text-transform: uppercase; - - &.common { - background-color: #6b6b6b; - color: #fff; - } - - &.rare { - background-color: #4b69ff; - color: #fff; - } - - &.legendary { - background-color: #d32ce6; - color: #fff; - } - } - - &:hover .lootbox-image { - transform: scale(1.05); - } -} - -.lootbox-info { - padding: 1.5rem; -} - -.lootbox-name { - font-size: 1.25rem; - margin: 0 0 1rem 0; - font-weight: 600; -} - -.lootbox-details { - margin-bottom: 1.5rem; - - .detail-item { - display: flex; - justify-content: space-between; - margin-bottom: 0.5rem; - - .label { - color: #aaa; - font-size: 0.9rem; - } - - .value { - font-weight: 600; - font-size: 0.9rem; - } - } -} - -.kaufen-btn { - width: 100%; - background-color: #ffcc00; - color: #000; - border: none; - border-radius: 4px; - padding: 0.75rem 0; - font-weight: 600; - cursor: pointer; - transition: background-color 0.2s ease; - text-transform: uppercase; - letter-spacing: 1px; - - &:hover { - background-color: #ffd633; - } -} \ No newline at end of file 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 index 43fe751..1382b23 100644 --- a/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts +++ b/frontend/src/app/feature/lootboxes/lootbox-selection/lootbox-selection.component.ts @@ -1,71 +1,134 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; - -interface Lootbox { - id: string; - name: string; - category: 'common' | 'rare' | 'legendary'; - price: number; - image: string; - chance: number; - maxPrize: number; -} +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, RouterModule], + imports: [CommonModule, NavbarComponent], templateUrl: './lootbox-selection.component.html', - styleUrl: './lootbox-selection.component.scss' + styleUrls: ['./lootbox-selection.component.css'] }) -export default class LootboxSelectionComponent { - lootboxes: Lootbox[] = [ +export default class LootboxSelectionComponent implements OnInit { + lootboxes: LootBox[] = []; + isLoading = true; + error = ''; + + // Fallback data in case the API call fails + fallbackLootboxes: LootBox[] = [ { - id: 'common-box', - name: 'Gewöhnliche Box', - category: 'common', - price: 5, - image: '/assets/images/lootboxes/common-box.png', - chance: 7, - maxPrize: 50 + id: 1, + name: "Basic LootBox", + price: 2.00, + rewards: [ + { + id: 1, + value: 0.50, + probability: 0.70 + }, + { + id: 5, + value: 5.00, + probability: 0.30 + } + ] }, { - id: 'rare-box', - name: 'Seltene Box', - category: 'rare', - price: 20, - image: '/assets/images/lootboxes/rare-box.png', - chance: 3.5, - maxPrize: 200 + id: 2, + name: "Premium LootBox", + price: 5.00, + rewards: [ + { + id: 4, + value: 2.00, + probability: 0.60 + }, + { + id: 5, + value: 5.00, + probability: 0.30 + }, + { + id: 6, + value: 15.00, + probability: 0.10 + } + ] }, { - id: 'legendary-box', - name: 'Legendäre Box', - category: 'legendary', - price: 50, - image: '/assets/images/lootboxes/legendary-box.png', - chance: 1, - maxPrize: 1000 + id: 3, + name: "Legendäre LootBox", + price: 15.00, + rewards: [ + { + id: 4, + value: 2.00, + probability: 0.60 + }, + { + id: 5, + value: 5.00, + probability: 0.30 + }, + { + id: 6, + value: 15.00, + probability: 0.10 + } + ] } ]; - categories = [ - { id: 'common', name: 'Gewöhnlich' }, - { id: 'rare', name: 'Selten' }, - { id: 'legendary', name: 'Legendär' } - ]; + constructor(private lootboxService: LootboxService, private router: Router, private cdr: ChangeDetectorRef) {} - selectedCategory: string | null = null; - - filterByCategory(category: string | null): void { - this.selectedCategory = category; + ngOnInit(): void { + this.loadLootboxes(); } - get filteredLootboxes(): Lootbox[] { - if (!this.selectedCategory) { - return this.lootboxes; + 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 } - return this.lootboxes.filter(box => box.category === this.selectedCategory); + } + + formatProbability(probability: number): string { + return (probability * 100).toFixed(0) + '%'; } } \ No newline at end of file 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..040b55d --- /dev/null +++ b/frontend/src/app/feature/lootboxes/services/lootbox.service.ts @@ -0,0 +1,33 @@ +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; + }) + ); + } +} \ No newline at end of file diff --git a/frontend/src/app/model/LootBox.ts b/frontend/src/app/model/LootBox.ts new file mode 100644 index 0000000..f1547b1 --- /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[]; +} \ No newline at end of file