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 = `Stufe ${rarity}`; + 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?.name || ''} + ${card?.attack != null ? `${card.attack}` : ""} + ${card?.defends != null ? `${card.defends}` : ""} + ${card?.cooldown!= null ? `${card.cooldown}` : ""} + ${card?.rarity ? `
${rarityImgs(card.rarity, 11)}
` : ""} +
${card?.name || ''}
+ `; +} + +/* ================================ + Haupt-Export +================================ */ export async function loadEvents() { const body = document.getElementById("qm-body-events"); if (!body) return; - /* ================================ - CSS einmalig laden - ================================ */ if (!document.querySelector('link[href="/css/events.css"]')) { const link = document.createElement("link"); link.rel = "stylesheet"; @@ -12,9 +47,6 @@ export async function loadEvents() { document.head.appendChild(link); } - /* ================================ - Event-Daten - ================================ */ const events = [ { id: 1, img: "/images/items/runenhaufen.png", label: "Booster Öffnen", type: "booster" }, { id: 2, img: "/images/items/runenhaufen.png", label: "Textzeile 2" }, @@ -23,9 +55,6 @@ export async function loadEvents() { { id: 5, img: "/images/items/runenhaufen.png", label: "Textzeile 5" }, ]; - /* ================================ - Haupt-HTML - ================================ */ body.innerHTML = `
${events.map(ev => ` @@ -37,7 +66,7 @@ export async function loadEvents() {
`).join("")} - + @@ -68,9 +96,6 @@ export async function loadEvents() { `; - /* ================================ - Referenzen - ================================ */ const overlay = body.querySelector("#event-detail-overlay"); const edpImg = body.querySelector("#edp-img"); const edpTitle = body.querySelector("#edp-title"); @@ -78,9 +103,7 @@ export async function loadEvents() { const boosterUi = body.querySelector("#booster-ui"); const eventsGrid = body.querySelector("#events-grid"); - /* ================================ - Event-Karten Klick - ================================ */ + /* ── Event-Karten ── */ body.querySelectorAll(".event-card").forEach(card => { card.addEventListener("click", () => { if (card.dataset.type === "booster") { @@ -100,15 +123,9 @@ export async function loadEvents() { }); }); - /* ================================ - Detail-Popup schließen - ================================ */ body.querySelector("#edp-close-btn").addEventListener("click", () => overlay.classList.remove("active")); overlay.addEventListener("click", e => { if (e.target === overlay) overlay.classList.remove("active"); }); - /* ================================ - Zurück-Button - ================================ */ body.querySelector("#booster-back-btn").addEventListener("click", () => { eventsGrid.style.display = ""; boosterUi.style.display = "none"; @@ -125,9 +142,7 @@ export async function loadEvents() { } }); - /* ================================ - Booster Zustand - ================================ */ + /* ── Booster Zustand ── */ let allCards = []; let isSpinning = false; let spinIntervals = []; @@ -137,80 +152,61 @@ export async function loadEvents() { spinIntervals = []; } - /* ================================ - Karten vorladen - ================================ */ async function preloadCards() { if (allCards.length) return; try { const res = await fetch("/api/booster/cards"); - allCards = await res.json(); + if (!res.ok) throw new Error(res.status); + allCards = await res.json(); } catch (e) { console.error("Karten laden fehlgeschlagen", e); } } - /* ================================ - Booster zurücksetzen - ================================ */ function resetBooster() { clearAllIntervals(); isSpinning = false; for (let i = 0; i < 5; i++) { - const slot = body.querySelector(`#booster-slot-${i}`); - slot.querySelector(".booster-slot-img").src = "/images/items/rueckseite.png"; - slot.querySelector(".booster-slot-name").textContent = ""; - slot.classList.remove("revealed", "spinning"); + const inner = body.querySelector(`#booster-slot-${i} .booster-slot-inner`); + inner.innerHTML = `?`; + 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 });