diff --git a/public/css/events.css b/public/css/events.css index 693f2d2..4396e27 100644 --- a/public/css/events.css +++ b/public/css/events.css @@ -1,23 +1,7 @@ -/* ================================================ - events.css – Tägliche Events Quickmenü-Popup - Die Popup-Basisgrösse kommt von .qm-popup in - quickmenu.css – hier wird sie per ID überschrieben. -================================================ */ +/* ================================ + Events Grid +================================ */ -/* Popup-Grösse für Events (ID schlägt Klasse) */ -#qm-popup-events { - width: 900px; - height: 600px; -} - -/* Body-Padding anpassen (war auf 1800px ausgelegt) */ -#qm-popup-events .qm-popup-body { - padding: 20px 30px; - align-items: flex-start; - justify-content: flex-start; -} - -/* ── Event Grid ─────────────────────────────────── */ .events-grid { display: flex; gap: 10px; @@ -98,14 +82,8 @@ } @keyframes eventDetailIn { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } } #event-detail-popup .edp-close { @@ -121,9 +99,7 @@ transition: color 0.1s; } -#event-detail-popup .edp-close:hover { - color: #fff; -} +#event-detail-popup .edp-close:hover { color: #fff; } #event-detail-popup .edp-img { display: block; @@ -148,3 +124,156 @@ line-height: 1.6; text-align: center; } + +/* ================================ + Booster UI +================================ */ + +.booster-ui { + flex-direction: column; + gap: 16px; + padding: 10px 0; + width: 100%; +} + +.booster-back-btn { + align-self: flex-start; + background: none; + border: 1px solid rgba(255, 200, 80, 0.3); + color: #c8960c; + font-size: 12px; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-family: "Cinzel", serif; + transition: background 0.15s, color 0.15s; +} + +.booster-back-btn:hover { + background: rgba(200, 150, 12, 0.15); + color: #f0d060; +} + +.booster-stage { + display: flex; + align-items: center; + gap: 24px; + width: 100%; +} + +/* ── Stapel links ── */ + +.booster-left { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.booster-stapel-img { + width: 110px; + height: auto; + cursor: pointer; + image-rendering: pixelated; + filter: drop-shadow(0 4px 12px rgba(200, 150, 12, 0.4)); + transition: transform 0.15s ease, filter 0.15s ease; +} + +.booster-stapel-img:hover:not(.used) { + transform: scale(1.06) translateY(-3px); + filter: drop-shadow(0 6px 18px rgba(240, 200, 60, 0.65)); +} + +.booster-stapel-img.used { + cursor: default; + filter: drop-shadow(0 2px 6px rgba(0,0,0,0.3)); +} + +.booster-stapel-hint { + font-size: 11px; + color: #a08040; + font-family: "Cinzel", serif; + letter-spacing: 1px; + text-align: center; +} + +/* ── Karten-Slots rechts ── */ + +.booster-slots { + display: flex; + gap: 10px; + flex: 1; + justify-content: center; +} + +.booster-slot { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} + +.booster-slot-inner { + width: 100%; + aspect-ratio: 2 / 3; + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + overflow: hidden; + background: #0d0a06; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +/* Dreh-Animation */ +.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; + box-shadow: + 0 0 16px rgba(200, 150, 12, 0.45), + inset 0 0 8px rgba(200, 150, 12, 0.1); + animation: revealPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +@keyframes revealPop { + 0% { transform: scale(0.85); opacity: 0.5; } + 60% { transform: scale(1.06); opacity: 1; } + 100% { transform: scale(1); opacity: 1; } +} + +.booster-slot-img { + width: 100%; + height: 100%; + object-fit: cover; + image-rendering: pixelated; + display: block; +} + +.booster-slot-name { + font-size: 10px; + color: #c8960c; + text-align: center; + font-family: "Cinzel", serif; + letter-spacing: 0.5px; + min-height: 14px; + word-break: break-word; +} diff --git a/public/images/items/rueckseite.png b/public/images/items/rueckseite.png new file mode 100644 index 0000000..a7c8036 Binary files /dev/null and b/public/images/items/rueckseite.png differ diff --git a/public/js/quickmenu/events.js b/public/js/quickmenu/events.js index 626a982..23e95dd 100644 --- a/public/js/quickmenu/events.js +++ b/public/js/quickmenu/events.js @@ -3,56 +3,61 @@ export async function loadEvents() { if (!body) return; /* ================================ - Event-Daten (hier später befüllen) + CSS einmalig laden + ================================ */ + if (!document.querySelector('link[href="/css/events.css"]')) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "/css/events.css"; + document.head.appendChild(link); + } + + /* ================================ + Event-Daten ================================ */ const events = [ - { - id: 1, - img: "/images/items/runenhaufen.png", - label: "Werfe heute deine Runen", - }, - { - id: 2, - img: "/images/items/runenhaufen.png", - label: "Spiele heute ein 1v1", - }, - { - id: 3, - img: "/images/items/runenhaufen.png", - label: "Spiele heute ein 2v2", - }, - { - id: 4, - img: "/images/items/runenhaufen.png", - label: "Spende ein wenig Holz.", - }, - { - id: 5, - img: "/images/items/runenhaufen.png", - label: "Spende ein wenig Gold.", - }, + { id: 1, img: "/images/items/runenhaufen.png", label: "Booster Öffnen", type: "booster" }, + { id: 2, img: "/images/items/runenhaufen.png", label: "Textzeile 2" }, + { id: 3, img: "/images/items/runenhaufen.png", label: "Textzeile 3" }, + { id: 4, img: "/images/items/runenhaufen.png", label: "Textzeile 4" }, + { id: 5, img: "/images/items/runenhaufen.png", label: "Textzeile 5" }, ]; /* ================================ - Haupt-HTML injizieren + Haupt-HTML ================================ */ body.innerHTML = ` - -
- ${events - .map( - (ev) => ` -
+
+ ${events.map(ev => ` +
${ev.label}
${ev.label} -
`, - ) - .join("")} +
`).join("")}
- + + + +
@@ -64,39 +69,188 @@ export async function loadEvents() { `; /* ================================ - Event-Karten klickbar machen + Referenzen ================================ */ + const overlay = body.querySelector("#event-detail-overlay"); + const edpImg = body.querySelector("#edp-img"); + const edpTitle = body.querySelector("#edp-title"); + const edpBody = body.querySelector("#edp-body"); + const boosterUi = body.querySelector("#booster-ui"); + const eventsGrid = body.querySelector("#events-grid"); - const overlay = body.querySelector("#event-detail-overlay"); - const edpImg = body.querySelector("#edp-img"); - const edpTitle = body.querySelector("#edp-title"); - const edpBody = body.querySelector("#edp-body"); - - body.querySelectorAll(".event-card").forEach((card) => { + /* ================================ + Event-Karten Klick + ================================ */ + body.querySelectorAll(".event-card").forEach(card => { card.addEventListener("click", () => { + if (card.dataset.type === "booster") { + eventsGrid.style.display = "none"; + boosterUi.style.display = "flex"; + resetBooster(); + return; + } const id = Number(card.dataset.eventId); - const ev = events.find((e) => e.id === id); + const ev = events.find(e => e.id === id); if (!ev) return; - - edpImg.src = ev.img; - edpImg.alt = ev.label; + edpImg.src = ev.img; + edpImg.alt = ev.label; edpTitle.textContent = ev.label; - edpBody.textContent = "Inhalt folgt..."; // hier später befüllen - + edpBody.textContent = "Inhalt folgt..."; overlay.classList.add("active"); }); }); - /* Detail-Popup schließen */ - body.querySelector("#edp-close-btn").addEventListener("click", () => { - overlay.classList.remove("active"); + /* ================================ + 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"; + clearAllIntervals(); + isSpinning = false; }); - overlay.addEventListener("click", (e) => { - if (e.target === overlay) overlay.classList.remove("active"); + document.addEventListener("keydown", e => { + if (e.key === "Escape") { + overlay.classList.remove("active"); + eventsGrid.style.display = ""; + boosterUi.style.display = "none"; + clearAllIntervals(); + } }); - document.addEventListener("keydown", function edpEsc(e) { - if (e.key === "Escape") overlay.classList.remove("active"); + /* ================================ + Booster Zustand + ================================ */ + let allCards = []; + let isSpinning = false; + let spinIntervals = []; + + function clearAllIntervals() { + spinIntervals.forEach(id => clearInterval(id)); + spinIntervals = []; + } + + /* ================================ + Karten vorladen + ================================ */ + async function preloadCards() { + if (allCards.length) return; + try { + const res = await fetch("/api/booster/cards"); + 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 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 + ================================ */ + function startSpinSlot(index) { + const slot = body.querySelector(`#booster-slot-${index}`); + const imgEl = slot.querySelector(".booster-slot-img"); + 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); + + spinIntervals[index] = iv; + } + + /* ================================ + 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"); + + slot.classList.remove("spinning"); + slot.classList.add("revealed"); + + imgEl.src = card?.image ? `/images/cards/${card.image}` : "/images/items/rueckseite.png"; + nameEl.textContent = card?.name || "???"; + } + + /* ================================ + Booster-Stapel Klick → Öffnen + ================================ */ + body.querySelector("#booster-stapel").addEventListener("click", async () => { + if (isSpinning) return; + if (!allCards.length) await preloadCards(); + if (!allCards.length) return; + + isSpinning = true; + + const stapel = body.querySelector("#booster-stapel"); + stapel.classList.add("used"); + stapel.style.opacity = "0.35"; + 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" }); + const data = await res.json(); + drawnCards = data.cards || []; + } catch (e) { + console.error("Booster öffnen fehlgeschlagen", e); + resetBooster(); + 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 new file mode 100644 index 0000000..5e38a1d --- /dev/null +++ b/routes/booster.js @@ -0,0 +1,143 @@ +const express = require("express"); +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; + for (const entry of weights) { + r -= entry.weight; + if (r <= 0) return entry.maxLevel; + } + return weights[weights.length - 1].maxLevel; +} + +/* ================================ + Gewichte je Spielerlevel +================================ */ + +function getWeights(playerLevel) { + if (playerLevel < 10) return [ + { maxLevel: 1, weight: 85 }, + { maxLevel: 2, weight: 15 }, + ]; + if (playerLevel < 20) return [ + { maxLevel: 1, weight: 65 }, + { maxLevel: 2, weight: 27 }, + { maxLevel: 3, weight: 8 }, + ]; + if (playerLevel < 30) return [ + { maxLevel: 1, weight: 55 }, + { maxLevel: 2, weight: 26 }, + { maxLevel: 3, weight: 13 }, + { maxLevel: 4, weight: 6 }, + ]; + if (playerLevel < 40) return [ + { maxLevel: 1, weight: 50 }, + { maxLevel: 2, weight: 25 }, + { maxLevel: 3, weight: 14 }, + { 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 }, + { maxLevel: 3, weight: 15 }, + { maxLevel: 4, weight: 8 }, + { maxLevel: 5, weight: 4.5 }, + { maxLevel: 6, weight: 0.5 }, + ]; +} + +/* ================================ + GET /api/booster/cards + Alle Karten für die 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" + ); + res.json(cards); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ================================ + POST /api/booster/open + Gibt 5 gewichtete Zufallskarten zurück +================================ */ + +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] + ); + 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 <= ?", + [maxAllowed] + ); + + 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); + 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); + } + + res.json({ cards: result, playerLevel }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +module.exports = router;