diff --git a/public/css/events.css b/public/css/events.css
index 7883f3e..e9ba778 100644
--- a/public/css/events.css
+++ b/public/css/events.css
@@ -1,7 +1,6 @@
/* ================================
Events Grid
================================ */
-
.events-grid {
display: flex;
gap: 10px;
@@ -50,9 +49,8 @@
}
/* ================================
- Detail-Popup (Overlay)
+ Detail-Popup
================================ */
-
#event-detail-overlay {
display: none;
position: fixed;
@@ -88,50 +86,34 @@
#event-detail-popup .edp-close {
position: absolute;
- top: 6px;
- right: 8px;
- background: none;
- border: none;
- color: #888;
- font-size: 11px;
- cursor: pointer;
- line-height: 1;
+ top: 6px; right: 8px;
+ background: none; border: none;
+ color: #888; font-size: 11px; cursor: pointer; line-height: 1;
transition: color 0.1s;
}
-
#event-detail-popup .edp-close:hover { color: #fff; }
#event-detail-popup .edp-img {
- display: block;
- margin: 0 auto 8px;
- width: 44px;
- height: 44px;
- object-fit: contain;
- image-rendering: pixelated;
+ display: block; margin: 0 auto 8px;
+ width: 44px; height: 44px;
+ object-fit: contain; image-rendering: pixelated;
}
#event-detail-popup .edp-title {
- text-align: center;
- font-size: 11px;
- font-weight: bold;
- color: #f5c842;
- margin-bottom: 6px;
+ text-align: center; font-size: 11px; font-weight: bold;
+ color: #f5c842; margin-bottom: 6px;
}
#event-detail-popup .edp-body {
- font-size: 10px;
- color: #cccccc;
- line-height: 1.6;
- text-align: center;
+ font-size: 10px; color: #cccccc; line-height: 1.6; text-align: center;
}
/* ================================
- Booster UI
+ Booster UI – Container
================================ */
-
.booster-ui {
flex-direction: column;
- gap: 16px;
+ gap: 14px;
padding: 10px 0;
width: 100%;
}
@@ -148,12 +130,12 @@
font-family: "Cinzel", serif;
transition: background 0.15s, color 0.15s;
}
-
.booster-back-btn:hover {
background: rgba(200, 150, 12, 0.15);
color: #f0d060;
}
+/* Alle 6 Elemente (Stapel + 5 Slots) in einem einzigen Flex-Container */
.booster-stage {
display: flex;
align-items: flex-start;
@@ -161,6 +143,7 @@
width: 100%;
}
+/* ── Stapel ── */
.booster-left {
display: flex;
flex-direction: column;
@@ -206,11 +189,11 @@
display: contents;
}
+/* ── Einzelner Slot ── */
.booster-slot {
display: flex;
flex-direction: column;
align-items: center;
- gap: 6px;
flex: 1;
min-width: 0;
}
@@ -228,22 +211,12 @@
position: relative;
}
-/* Dreh-Animation */
+/* Drehen */
.booster-slot.spinning .booster-slot-inner {
border-color: rgba(200, 150, 12, 0.5);
box-shadow: 0 0 12px rgba(200, 150, 12, 0.25);
}
-.booster-slot.spinning .booster-slot-img {
- animation: slotFlicker 0.08s steps(1) infinite;
-}
-
-@keyframes slotFlicker {
- 0% { opacity: 1; }
- 50% { opacity: 0.75; }
- 100% { opacity: 1; }
-}
-
/* Enthüllt */
.booster-slot.revealed .booster-slot-inner {
border-color: #c8960c;
@@ -267,12 +240,97 @@
display: block;
}
-.booster-slot-name {
- font-size: 10px;
- color: #c8960c;
- text-align: center;
+/* ================================
+ Karten-Stats innerhalb der Slots
+ (identisch mit carddeck.js)
+================================ */
+
+/* Angriff – rechts mittig */
+.bs-stat-atk {
+ position: absolute;
+ right: 6px;
+ top: 40%;
+ transform: translateY(-50%);
+ background: rgba(180, 40, 20, 0.88);
+ border: 1px solid #ff6040;
+ border-radius: 45px;
+ color: #fff;
font-family: "Cinzel", serif;
- letter-spacing: 0.5px;
- min-height: 14px;
- word-break: break-word;
+ font-size: 9px;
+ font-weight: bold;
+ padding: 1px 4px;
+ z-index: 5;
+ pointer-events: none;
+}
+
+/* Verteidigung – links mittig */
+.bs-stat-def {
+ position: absolute;
+ left: 6px;
+ top: 40%;
+ transform: translateY(-50%);
+ background: rgba(20, 80, 180, 0.88);
+ border: 1px solid #4090ff;
+ border-radius: 45px;
+ color: #fff;
+ font-family: "Cinzel", serif;
+ font-size: 9px;
+ font-weight: bold;
+ padding: 1px 4px;
+ z-index: 5;
+ pointer-events: none;
+}
+
+/* Cooldown – unten rechts, Kreis */
+.bs-stat-cd {
+ position: absolute;
+ bottom: 6px;
+ right: 5px;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.75);
+ border: 1px solid #f0d060;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: "Cinzel", serif;
+ font-size: 8px;
+ font-weight: bold;
+ color: #f0d9a6;
+ z-index: 5;
+ pointer-events: none;
+}
+
+/* Kristalle – mittig unten */
+.bs-rarity {
+ position: absolute;
+ top: 72%;
+ left: 0;
+ right: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 1px;
+ z-index: 5;
+ pointer-events: none;
+}
+
+/* Kartenname – ganz unten */
+.bs-card-name {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 2px 3px;
+ background: linear-gradient(transparent, rgba(0,0,0,0.82));
+ font-family: "Cinzel", serif;
+ font-size: 7px;
+ color: #f0d9a6;
+ text-align: center;
+ line-height: 1.3;
+ border-radius: 0 0 4px 4px;
+ z-index: 5;
+ pointer-events: none;
}
diff --git a/public/js/quickmenu/events.js b/public/js/quickmenu/events.js
index 23e95dd..de18dd1 100644
--- a/public/js/quickmenu/events.js
+++ b/public/js/quickmenu/events.js
@@ -1,10 +1,45 @@
+/* ================================
+ Kristall-Mapping (aus carddeck.js)
+================================ */
+const RARITY_CRYSTALS = {
+ 1: "roter-cristal.png",
+ 2: "blauer-cristal.png",
+ 3: "gelber-cristal.png",
+ 4: "gruener-cristal.png",
+ 5: "oranger-cristal.png",
+ 6: "violet-cristal.png",
+ 7: "pinker-cristal.png",
+};
+
+function rarityImgs(rarity, size = 13) {
+ const file = RARITY_CRYSTALS[String(rarity)];
+ if (!file) return "";
+ const count = parseInt(rarity) || 0;
+ const img = ``;
+ return img.repeat(count);
+}
+
+function cardHTML(card, isFront = true) {
+ if (!isFront) return `
`;
+
+ const img = card?.image ? `/images/cards/${card.image}` : "/images/items/rueckseite.png";
+ return `
+
+ ${card?.attack != null ? `${card.attack}` : ""}
+ ${card?.defends != null ? `${card.defends}` : ""}
+ ${card?.cooldown!= null ? `${card.cooldown}` : ""}
+ ${card?.rarity ? `
`;
+ body.querySelector(`#booster-slot-${i}`).classList.remove("revealed", "spinning");
}
const stapel = body.querySelector("#booster-stapel");
stapel.classList.remove("used");
stapel.style.opacity = "1";
stapel.style.cursor = "pointer";
-
body.querySelector("#booster-hint").textContent = "Klicken zum Öffnen";
-
preloadCards();
}
- /* ================================
- Slot drehen lassen
- ================================ */
+ /* ── Slot drehen – 350ms, damit man die Karten erkennt ── */
function startSpinSlot(index) {
const slot = body.querySelector(`#booster-slot-${index}`);
- const imgEl = slot.querySelector(".booster-slot-img");
+ const inner = slot.querySelector(".booster-slot-inner");
slot.classList.add("spinning");
const iv = setInterval(() => {
if (!allCards.length) return;
const rnd = allCards[Math.floor(Math.random() * allCards.length)];
- imgEl.src = rnd.image ? `/images/cards/${rnd.image}` : "/images/items/rueckseite.png";
- }, 80);
+ inner.innerHTML = cardHTML(rnd, true);
+ }, 350);
spinIntervals[index] = iv;
}
- /* ================================
- Slot enthüllen
- ================================ */
+ /* ── Slot enthüllen ── */
function revealSlot(index, card) {
clearInterval(spinIntervals[index]);
-
- const slot = body.querySelector(`#booster-slot-${index}`);
- const imgEl = slot.querySelector(".booster-slot-img");
- const nameEl = slot.querySelector(".booster-slot-name");
-
+ const slot = body.querySelector(`#booster-slot-${index}`);
+ const inner = slot.querySelector(".booster-slot-inner");
slot.classList.remove("spinning");
slot.classList.add("revealed");
-
- imgEl.src = card?.image ? `/images/cards/${card.image}` : "/images/items/rueckseite.png";
- nameEl.textContent = card?.name || "???";
+ inner.innerHTML = cardHTML(card, true);
}
- /* ================================
- Booster-Stapel Klick → Öffnen
- ================================ */
+ /* ── Stapel klicken ── */
body.querySelector("#booster-stapel").addEventListener("click", async () => {
if (isSpinning) return;
if (!allCards.length) await preloadCards();
@@ -224,7 +220,6 @@ export async function loadEvents() {
stapel.style.cursor = "default";
body.querySelector("#booster-hint").textContent = "Wird gezogen...";
- // 5 Karten vom Server ziehen
let drawnCards = [];
try {
const res = await fetch("/api/booster/open", { method: "POST" });
@@ -236,21 +231,17 @@ export async function loadEvents() {
return;
}
- // Alle Slots gleichzeitig drehen
for (let i = 0; i < 5; i++) startSpinSlot(i);
- // Nacheinander alle 5 Sekunden eine Karte enthüllen
for (let i = 0; i < 5; i++) {
setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000);
}
- // Nach letzter Karte: Fertig-Meldung
setTimeout(() => {
body.querySelector("#booster-hint").textContent = "Alle Karten enthüllt!";
isSpinning = false;
}, 5 * 5000 + 500);
});
- // Karten sofort vorladen
preloadCards();
}
diff --git a/routes/booster.js b/routes/booster.js
index 5e38a1d..06f6ae1 100644
--- a/routes/booster.js
+++ b/routes/booster.js
@@ -1,11 +1,10 @@
const express = require("express");
-const router = express.Router();
-const db = require("../database/database");
+const router = express.Router();
+const db = require("../database/database");
/* ================================
Gewichtete Zufallsauswahl
================================ */
-
function weightedRandom(weights) {
const total = weights.reduce((s, w) => s + w.weight, 0);
let r = Math.random() * total;
@@ -19,7 +18,6 @@ function weightedRandom(weights) {
/* ================================
Gewichte je Spielerlevel
================================ */
-
function getWeights(playerLevel) {
if (playerLevel < 10) return [
{ maxLevel: 1, weight: 85 },
@@ -43,14 +41,6 @@ function getWeights(playerLevel) {
{ maxLevel: 4, weight: 7 },
{ maxLevel: 5, weight: 4 },
];
- if (playerLevel < 50) return [
- { maxLevel: 1, weight: 47 },
- { maxLevel: 2, weight: 25 },
- { maxLevel: 3, weight: 15 },
- { maxLevel: 4, weight: 8 },
- { maxLevel: 5, weight: 4.5 },
- { maxLevel: 6, weight: 0.5 },
- ];
return [
{ maxLevel: 1, weight: 47 },
{ maxLevel: 2, weight: 25 },
@@ -63,15 +53,13 @@ function getWeights(playerLevel) {
/* ================================
GET /api/booster/cards
- Alle Karten für die Slot-Animation
+ Alle Karten inkl. Stats für Slot-Animation
================================ */
-
router.get("/booster/cards", async (req, res) => {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
-
try {
const [cards] = await db.query(
- "SELECT id, name, image, max_level, rarity FROM cards ORDER BY id"
+ "SELECT id, name, image, max_level, rarity, attack, defends, cooldown FROM cards ORDER BY id"
);
res.json(cards);
} catch (err) {
@@ -82,55 +70,39 @@ router.get("/booster/cards", async (req, res) => {
/* ================================
POST /api/booster/open
- Gibt 5 gewichtete Zufallskarten zurück
+ 5 gewichtete Zufallskarten inkl. Stats
================================ */
-
router.post("/booster/open", async (req, res) => {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
const userId = req.session.user.id;
-
try {
- // Spielerlevel direkt aus accounts
const [[account]] = await db.query(
- "SELECT level FROM accounts WHERE id = ?",
- [userId]
+ "SELECT level FROM accounts WHERE id = ?", [userId]
);
const playerLevel = account?.level ?? 1;
const weights = getWeights(playerLevel);
const maxAllowed = Math.max(...weights.map(w => w.maxLevel));
- // Alle erlaubten Karten laden
const [allCards] = await db.query(
- "SELECT id, name, image, max_level, rarity FROM cards WHERE max_level <= ?",
+ "SELECT id, name, image, max_level, rarity, attack, defends, cooldown FROM cards WHERE max_level <= ?",
[maxAllowed]
);
- if (!allCards.length) {
- return res.status(400).json({ error: "Keine Karten verfügbar" });
- }
+ if (!allCards.length) return res.status(400).json({ error: "Keine Karten verfügbar" });
- // 5 Karten zufällig ziehen
const result = [];
for (let i = 0; i < 5; i++) {
const targetLevel = weightedRandom(weights);
-
- // Karten mit diesem max_level filtern
let pool = allCards.filter(c => c.max_level === targetLevel);
-
- // Fallback: nächstniedrigeres Level nehmen
if (!pool.length) {
- for (let fallback = targetLevel - 1; fallback >= 1; fallback--) {
- pool = allCards.filter(c => c.max_level === fallback);
+ for (let fb = targetLevel - 1; fb >= 1; fb--) {
+ pool = allCards.filter(c => c.max_level === fb);
if (pool.length) break;
}
}
-
- // Fallback: irgendeine erlaubte Karte
if (!pool.length) pool = allCards;
-
- const card = pool[Math.floor(Math.random() * pool.length)];
- result.push(card);
+ result.push(pool[Math.floor(Math.random() * pool.length)]);
}
res.json({ cards: result, playerLevel });