This commit is contained in:
cay 2026-04-06 13:43:09 +01:00
parent 0d68978d4e
commit c988486d63
3 changed files with 174 additions and 153 deletions

View File

@ -1,7 +1,6 @@
/* ================================ /* ================================
Events Grid Events Grid
================================ */ ================================ */
.events-grid { .events-grid {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -50,9 +49,8 @@
} }
/* ================================ /* ================================
Detail-Popup (Overlay) Detail-Popup
================================ */ ================================ */
#event-detail-overlay { #event-detail-overlay {
display: none; display: none;
position: fixed; position: fixed;
@ -88,50 +86,34 @@
#event-detail-popup .edp-close { #event-detail-popup .edp-close {
position: absolute; position: absolute;
top: 6px; top: 6px; right: 8px;
right: 8px; background: none; border: none;
background: none; color: #888; font-size: 11px; cursor: pointer; line-height: 1;
border: none;
color: #888;
font-size: 11px;
cursor: pointer;
line-height: 1;
transition: color 0.1s; 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 { #event-detail-popup .edp-img {
display: block; display: block; margin: 0 auto 8px;
margin: 0 auto 8px; width: 44px; height: 44px;
width: 44px; object-fit: contain; image-rendering: pixelated;
height: 44px;
object-fit: contain;
image-rendering: pixelated;
} }
#event-detail-popup .edp-title { #event-detail-popup .edp-title {
text-align: center; text-align: center; font-size: 11px; font-weight: bold;
font-size: 11px; color: #f5c842; margin-bottom: 6px;
font-weight: bold;
color: #f5c842;
margin-bottom: 6px;
} }
#event-detail-popup .edp-body { #event-detail-popup .edp-body {
font-size: 10px; font-size: 10px; color: #cccccc; line-height: 1.6; text-align: center;
color: #cccccc;
line-height: 1.6;
text-align: center;
} }
/* ================================ /* ================================
Booster UI Booster UI Container
================================ */ ================================ */
.booster-ui { .booster-ui {
flex-direction: column; flex-direction: column;
gap: 16px; gap: 14px;
padding: 10px 0; padding: 10px 0;
width: 100%; width: 100%;
} }
@ -148,12 +130,12 @@
font-family: "Cinzel", serif; font-family: "Cinzel", serif;
transition: background 0.15s, color 0.15s; transition: background 0.15s, color 0.15s;
} }
.booster-back-btn:hover { .booster-back-btn:hover {
background: rgba(200, 150, 12, 0.15); background: rgba(200, 150, 12, 0.15);
color: #f0d060; color: #f0d060;
} }
/* Alle 6 Elemente (Stapel + 5 Slots) in einem einzigen Flex-Container */
.booster-stage { .booster-stage {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -161,6 +143,7 @@
width: 100%; width: 100%;
} }
/* ── Stapel ── */
.booster-left { .booster-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -206,11 +189,11 @@
display: contents; display: contents;
} }
/* ── Einzelner Slot ── */
.booster-slot { .booster-slot {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 6px;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
@ -228,22 +211,12 @@
position: relative; position: relative;
} }
/* Dreh-Animation */ /* Drehen */
.booster-slot.spinning .booster-slot-inner { .booster-slot.spinning .booster-slot-inner {
border-color: rgba(200, 150, 12, 0.5); border-color: rgba(200, 150, 12, 0.5);
box-shadow: 0 0 12px rgba(200, 150, 12, 0.25); 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 */ /* Enthüllt */
.booster-slot.revealed .booster-slot-inner { .booster-slot.revealed .booster-slot-inner {
border-color: #c8960c; border-color: #c8960c;
@ -267,12 +240,97 @@
display: block; display: block;
} }
.booster-slot-name { /* ================================
font-size: 10px; Karten-Stats innerhalb der Slots
color: #c8960c; (identisch mit carddeck.js)
text-align: center; ================================ */
/* 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; font-family: "Cinzel", serif;
letter-spacing: 0.5px; font-size: 9px;
min-height: 14px; font-weight: bold;
word-break: break-word; 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;
} }

View File

@ -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 = `<img src="/images/items/${file}" alt="Stufe ${rarity}" style="width:${size}px;height:${size}px;object-fit:contain;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.8));">`;
return img.repeat(count);
}
function cardHTML(card, isFront = true) {
if (!isFront) return `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
const img = card?.image ? `/images/cards/${card.image}` : "/images/items/rueckseite.png";
return `
<img class="booster-slot-img" src="${img}" alt="${card?.name || ''}" draggable="false">
${card?.attack != null ? `<span class="bs-stat-atk">${card.attack}</span>` : ""}
${card?.defends != null ? `<span class="bs-stat-def">${card.defends}</span>` : ""}
${card?.cooldown!= null ? `<span class="bs-stat-cd">${card.cooldown}</span>` : ""}
${card?.rarity ? `<div class="bs-rarity">${rarityImgs(card.rarity, 11)}</div>` : ""}
<div class="bs-card-name">${card?.name || ''}</div>
`;
}
/* ================================
Haupt-Export
================================ */
export async function loadEvents() { export async function loadEvents() {
const body = document.getElementById("qm-body-events"); const body = document.getElementById("qm-body-events");
if (!body) return; if (!body) return;
/* ================================
CSS einmalig laden
================================ */
if (!document.querySelector('link[href="/css/events.css"]')) { if (!document.querySelector('link[href="/css/events.css"]')) {
const link = document.createElement("link"); const link = document.createElement("link");
link.rel = "stylesheet"; link.rel = "stylesheet";
@ -12,9 +47,6 @@ export async function loadEvents() {
document.head.appendChild(link); document.head.appendChild(link);
} }
/* ================================
Event-Daten
================================ */
const events = [ const events = [
{ id: 1, img: "/images/items/runenhaufen.png", label: "Booster Öffnen", type: "booster" }, { id: 1, img: "/images/items/runenhaufen.png", label: "Booster Öffnen", type: "booster" },
{ id: 2, img: "/images/items/runenhaufen.png", label: "Textzeile 2" }, { 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" }, { id: 5, img: "/images/items/runenhaufen.png", label: "Textzeile 5" },
]; ];
/* ================================
Haupt-HTML
================================ */
body.innerHTML = ` body.innerHTML = `
<div class="events-grid" id="events-grid"> <div class="events-grid" id="events-grid">
${events.map(ev => ` ${events.map(ev => `
@ -37,7 +66,7 @@ export async function loadEvents() {
</div>`).join("")} </div>`).join("")}
</div> </div>
<!-- Booster Öffnen UI --> <!-- Booster UI -->
<div id="booster-ui" class="booster-ui" style="display:none;"> <div id="booster-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="booster-back-btn"> Zurück</button> <button class="booster-back-btn" id="booster-back-btn"> Zurück</button>
<div class="booster-stage"> <div class="booster-stage">
@ -51,7 +80,6 @@ export async function loadEvents() {
<div class="booster-slot-inner"> <div class="booster-slot-inner">
<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false"> <img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">
</div> </div>
<div class="booster-slot-name"></div>
</div>`).join("")} </div>`).join("")}
</div> </div>
</div> </div>
@ -68,9 +96,6 @@ export async function loadEvents() {
</div> </div>
`; `;
/* ================================
Referenzen
================================ */
const overlay = body.querySelector("#event-detail-overlay"); const overlay = body.querySelector("#event-detail-overlay");
const edpImg = body.querySelector("#edp-img"); const edpImg = body.querySelector("#edp-img");
const edpTitle = body.querySelector("#edp-title"); const edpTitle = body.querySelector("#edp-title");
@ -78,9 +103,7 @@ export async function loadEvents() {
const boosterUi = body.querySelector("#booster-ui"); const boosterUi = body.querySelector("#booster-ui");
const eventsGrid = body.querySelector("#events-grid"); const eventsGrid = body.querySelector("#events-grid");
/* ================================ /* ── Event-Karten ── */
Event-Karten Klick
================================ */
body.querySelectorAll(".event-card").forEach(card => { body.querySelectorAll(".event-card").forEach(card => {
card.addEventListener("click", () => { card.addEventListener("click", () => {
if (card.dataset.type === "booster") { 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")); body.querySelector("#edp-close-btn").addEventListener("click", () => overlay.classList.remove("active"));
overlay.addEventListener("click", e => { if (e.target === overlay) 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", () => { body.querySelector("#booster-back-btn").addEventListener("click", () => {
eventsGrid.style.display = ""; eventsGrid.style.display = "";
boosterUi.style.display = "none"; boosterUi.style.display = "none";
@ -125,9 +142,7 @@ export async function loadEvents() {
} }
}); });
/* ================================ /* ── Booster Zustand ── */
Booster Zustand
================================ */
let allCards = []; let allCards = [];
let isSpinning = false; let isSpinning = false;
let spinIntervals = []; let spinIntervals = [];
@ -137,80 +152,61 @@ export async function loadEvents() {
spinIntervals = []; spinIntervals = [];
} }
/* ================================
Karten vorladen
================================ */
async function preloadCards() { async function preloadCards() {
if (allCards.length) return; if (allCards.length) return;
try { try {
const res = await fetch("/api/booster/cards"); 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) { } catch (e) {
console.error("Karten laden fehlgeschlagen", e); console.error("Karten laden fehlgeschlagen", e);
} }
} }
/* ================================
Booster zurücksetzen
================================ */
function resetBooster() { function resetBooster() {
clearAllIntervals(); clearAllIntervals();
isSpinning = false; isSpinning = false;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const slot = body.querySelector(`#booster-slot-${i}`); const inner = body.querySelector(`#booster-slot-${i} .booster-slot-inner`);
slot.querySelector(".booster-slot-img").src = "/images/items/rueckseite.png"; inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
slot.querySelector(".booster-slot-name").textContent = ""; body.querySelector(`#booster-slot-${i}`).classList.remove("revealed", "spinning");
slot.classList.remove("revealed", "spinning");
} }
const stapel = body.querySelector("#booster-stapel"); const stapel = body.querySelector("#booster-stapel");
stapel.classList.remove("used"); stapel.classList.remove("used");
stapel.style.opacity = "1"; stapel.style.opacity = "1";
stapel.style.cursor = "pointer"; stapel.style.cursor = "pointer";
body.querySelector("#booster-hint").textContent = "Klicken zum Öffnen"; body.querySelector("#booster-hint").textContent = "Klicken zum Öffnen";
preloadCards(); preloadCards();
} }
/* ================================ /* ── Slot drehen 350ms, damit man die Karten erkennt ── */
Slot drehen lassen
================================ */
function startSpinSlot(index) { function startSpinSlot(index) {
const slot = body.querySelector(`#booster-slot-${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"); slot.classList.add("spinning");
const iv = setInterval(() => { const iv = setInterval(() => {
if (!allCards.length) return; if (!allCards.length) return;
const rnd = allCards[Math.floor(Math.random() * allCards.length)]; const rnd = allCards[Math.floor(Math.random() * allCards.length)];
imgEl.src = rnd.image ? `/images/cards/${rnd.image}` : "/images/items/rueckseite.png"; inner.innerHTML = cardHTML(rnd, true);
}, 80); }, 350);
spinIntervals[index] = iv; spinIntervals[index] = iv;
} }
/* ================================ /* ── Slot enthüllen ── */
Slot enthüllen
================================ */
function revealSlot(index, card) { function revealSlot(index, card) {
clearInterval(spinIntervals[index]); clearInterval(spinIntervals[index]);
const slot = body.querySelector(`#booster-slot-${index}`);
const slot = body.querySelector(`#booster-slot-${index}`); const inner = slot.querySelector(".booster-slot-inner");
const imgEl = slot.querySelector(".booster-slot-img");
const nameEl = slot.querySelector(".booster-slot-name");
slot.classList.remove("spinning"); slot.classList.remove("spinning");
slot.classList.add("revealed"); slot.classList.add("revealed");
inner.innerHTML = cardHTML(card, true);
imgEl.src = card?.image ? `/images/cards/${card.image}` : "/images/items/rueckseite.png";
nameEl.textContent = card?.name || "???";
} }
/* ================================ /* ── Stapel klicken ── */
Booster-Stapel Klick Öffnen
================================ */
body.querySelector("#booster-stapel").addEventListener("click", async () => { body.querySelector("#booster-stapel").addEventListener("click", async () => {
if (isSpinning) return; if (isSpinning) return;
if (!allCards.length) await preloadCards(); if (!allCards.length) await preloadCards();
@ -224,7 +220,6 @@ export async function loadEvents() {
stapel.style.cursor = "default"; stapel.style.cursor = "default";
body.querySelector("#booster-hint").textContent = "Wird gezogen..."; body.querySelector("#booster-hint").textContent = "Wird gezogen...";
// 5 Karten vom Server ziehen
let drawnCards = []; let drawnCards = [];
try { try {
const res = await fetch("/api/booster/open", { method: "POST" }); const res = await fetch("/api/booster/open", { method: "POST" });
@ -236,21 +231,17 @@ export async function loadEvents() {
return; return;
} }
// Alle Slots gleichzeitig drehen
for (let i = 0; i < 5; i++) startSpinSlot(i); for (let i = 0; i < 5; i++) startSpinSlot(i);
// Nacheinander alle 5 Sekunden eine Karte enthüllen
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000); setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000);
} }
// Nach letzter Karte: Fertig-Meldung
setTimeout(() => { setTimeout(() => {
body.querySelector("#booster-hint").textContent = "Alle Karten enthüllt!"; body.querySelector("#booster-hint").textContent = "Alle Karten enthüllt!";
isSpinning = false; isSpinning = false;
}, 5 * 5000 + 500); }, 5 * 5000 + 500);
}); });
// Karten sofort vorladen
preloadCards(); preloadCards();
} }

View File

@ -1,11 +1,10 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const db = require("../database/database"); const db = require("../database/database");
/* ================================ /* ================================
Gewichtete Zufallsauswahl Gewichtete Zufallsauswahl
================================ */ ================================ */
function weightedRandom(weights) { function weightedRandom(weights) {
const total = weights.reduce((s, w) => s + w.weight, 0); const total = weights.reduce((s, w) => s + w.weight, 0);
let r = Math.random() * total; let r = Math.random() * total;
@ -19,7 +18,6 @@ function weightedRandom(weights) {
/* ================================ /* ================================
Gewichte je Spielerlevel Gewichte je Spielerlevel
================================ */ ================================ */
function getWeights(playerLevel) { function getWeights(playerLevel) {
if (playerLevel < 10) return [ if (playerLevel < 10) return [
{ maxLevel: 1, weight: 85 }, { maxLevel: 1, weight: 85 },
@ -43,14 +41,6 @@ function getWeights(playerLevel) {
{ maxLevel: 4, weight: 7 }, { maxLevel: 4, weight: 7 },
{ maxLevel: 5, weight: 4 }, { 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 [ return [
{ maxLevel: 1, weight: 47 }, { maxLevel: 1, weight: 47 },
{ maxLevel: 2, weight: 25 }, { maxLevel: 2, weight: 25 },
@ -63,15 +53,13 @@ function getWeights(playerLevel) {
/* ================================ /* ================================
GET /api/booster/cards 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) => { router.get("/booster/cards", async (req, res) => {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" }); if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
try { try {
const [cards] = await db.query( 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); res.json(cards);
} catch (err) { } catch (err) {
@ -82,55 +70,39 @@ router.get("/booster/cards", async (req, res) => {
/* ================================ /* ================================
POST /api/booster/open POST /api/booster/open
Gibt 5 gewichtete Zufallskarten zurück 5 gewichtete Zufallskarten inkl. Stats
================================ */ ================================ */
router.post("/booster/open", async (req, res) => { router.post("/booster/open", async (req, res) => {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" }); if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
const userId = req.session.user.id; const userId = req.session.user.id;
try { try {
// Spielerlevel direkt aus accounts
const [[account]] = await db.query( const [[account]] = await db.query(
"SELECT level FROM accounts WHERE id = ?", "SELECT level FROM accounts WHERE id = ?", [userId]
[userId]
); );
const playerLevel = account?.level ?? 1; const playerLevel = account?.level ?? 1;
const weights = getWeights(playerLevel); const weights = getWeights(playerLevel);
const maxAllowed = Math.max(...weights.map(w => w.maxLevel)); const maxAllowed = Math.max(...weights.map(w => w.maxLevel));
// Alle erlaubten Karten laden
const [allCards] = await db.query( 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] [maxAllowed]
); );
if (!allCards.length) { if (!allCards.length) return res.status(400).json({ error: "Keine Karten verfügbar" });
return res.status(400).json({ error: "Keine Karten verfügbar" });
}
// 5 Karten zufällig ziehen
const result = []; const result = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const targetLevel = weightedRandom(weights); const targetLevel = weightedRandom(weights);
// Karten mit diesem max_level filtern
let pool = allCards.filter(c => c.max_level === targetLevel); let pool = allCards.filter(c => c.max_level === targetLevel);
// Fallback: nächstniedrigeres Level nehmen
if (!pool.length) { if (!pool.length) {
for (let fallback = targetLevel - 1; fallback >= 1; fallback--) { for (let fb = targetLevel - 1; fb >= 1; fb--) {
pool = allCards.filter(c => c.max_level === fallback); pool = allCards.filter(c => c.max_level === fb);
if (pool.length) break; if (pool.length) break;
} }
} }
// Fallback: irgendeine erlaubte Karte
if (!pool.length) pool = allCards; if (!pool.length) pool = allCards;
result.push(pool[Math.floor(Math.random() * pool.length)]);
const card = pool[Math.floor(Math.random() * pool.length)];
result.push(card);
} }
res.json({ cards: result, playerLevel }); res.json({ cards: result, playerLevel });