Boosterjagd
This commit is contained in:
parent
661c62dd64
commit
3f5b63b750
@ -1,23 +1,7 @@
|
|||||||
/* ================================================
|
/* ================================
|
||||||
events.css – Tägliche Events Quickmenü-Popup
|
Events Grid
|
||||||
Die Popup-Basisgrösse kommt von .qm-popup in
|
================================ */
|
||||||
quickmenu.css – hier wird sie per ID überschrieben.
|
|
||||||
================================================ */
|
|
||||||
|
|
||||||
/* 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 {
|
.events-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -98,14 +82,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes eventDetailIn {
|
@keyframes eventDetailIn {
|
||||||
from {
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
opacity: 0;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
transform: translateY(6px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#event-detail-popup .edp-close {
|
#event-detail-popup .edp-close {
|
||||||
@ -121,9 +99,7 @@
|
|||||||
transition: color 0.1s;
|
transition: color 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#event-detail-popup .edp-close:hover {
|
#event-detail-popup .edp-close:hover { color: #fff; }
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
#event-detail-popup .edp-img {
|
#event-detail-popup .edp-img {
|
||||||
display: block;
|
display: block;
|
||||||
@ -148,3 +124,156 @@
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
text-align: center;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
BIN
public/images/items/rueckseite.png
Normal file
BIN
public/images/items/rueckseite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
@ -3,56 +3,61 @@ export async function loadEvents() {
|
|||||||
if (!body) return;
|
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 = [
|
const events = [
|
||||||
{
|
{ id: 1, img: "/images/items/runenhaufen.png", label: "Booster Öffnen", type: "booster" },
|
||||||
id: 1,
|
{ id: 2, img: "/images/items/runenhaufen.png", label: "Textzeile 2" },
|
||||||
img: "/images/items/runenhaufen.png",
|
{ id: 3, img: "/images/items/runenhaufen.png", label: "Textzeile 3" },
|
||||||
label: "Werfe heute deine Runen",
|
{ id: 4, img: "/images/items/runenhaufen.png", label: "Textzeile 4" },
|
||||||
},
|
{ id: 5, img: "/images/items/runenhaufen.png", label: "Textzeile 5" },
|
||||||
{
|
|
||||||
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.",
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
Haupt-HTML injizieren
|
Haupt-HTML
|
||||||
================================ */
|
================================ */
|
||||||
body.innerHTML = `
|
body.innerHTML = `
|
||||||
<!-- 5er Event-Grid -->
|
<div class="events-grid" id="events-grid">
|
||||||
<div class="events-grid">
|
${events.map(ev => `
|
||||||
${events
|
<div class="event-card" data-event-id="${ev.id}" data-type="${ev.type || ''}">
|
||||||
.map(
|
|
||||||
(ev) => `
|
|
||||||
<div class="event-card" data-event-id="${ev.id}">
|
|
||||||
<div class="event-card-img-wrap">
|
<div class="event-card-img-wrap">
|
||||||
<img src="${ev.img}" alt="${ev.label}" draggable="false">
|
<img src="${ev.img}" alt="${ev.label}" draggable="false">
|
||||||
</div>
|
</div>
|
||||||
<span class="event-card-label">${ev.label}</span>
|
<span class="event-card-label">${ev.label}</span>
|
||||||
</div>`,
|
</div>`).join("")}
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail-Popup -->
|
<!-- Booster Öffnen UI -->
|
||||||
|
<div id="booster-ui" class="booster-ui" style="display:none;">
|
||||||
|
<button class="booster-back-btn" id="booster-back-btn">← Zurück</button>
|
||||||
|
<div class="booster-stage">
|
||||||
|
<div class="booster-left">
|
||||||
|
<img id="booster-stapel" src="/images/items/boosterstapel.png" alt="Booster" draggable="false" class="booster-stapel-img">
|
||||||
|
<span class="booster-stapel-hint" id="booster-hint">Klicken zum Öffnen</span>
|
||||||
|
</div>
|
||||||
|
<div class="booster-slots" id="booster-slots">
|
||||||
|
${Array.from({length: 5}, (_, i) => `
|
||||||
|
<div class="booster-slot" id="booster-slot-${i}">
|
||||||
|
<div class="booster-slot-inner">
|
||||||
|
<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">
|
||||||
|
</div>
|
||||||
|
<div class="booster-slot-name"></div>
|
||||||
|
</div>`).join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Standard Detail-Popup -->
|
||||||
<div id="event-detail-overlay">
|
<div id="event-detail-overlay">
|
||||||
<div id="event-detail-popup">
|
<div id="event-detail-popup">
|
||||||
<button class="edp-close" id="edp-close-btn">✕</button>
|
<button class="edp-close" id="edp-close-btn">✕</button>
|
||||||
@ -64,39 +69,188 @@ export async function loadEvents() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
Event-Karten klickbar machen
|
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");
|
||||||
const edpBody = body.querySelector("#edp-body");
|
const edpBody = body.querySelector("#edp-body");
|
||||||
|
const boosterUi = body.querySelector("#booster-ui");
|
||||||
|
const eventsGrid = body.querySelector("#events-grid");
|
||||||
|
|
||||||
body.querySelectorAll(".event-card").forEach((card) => {
|
/* ================================
|
||||||
|
Event-Karten Klick
|
||||||
|
================================ */
|
||||||
|
body.querySelectorAll(".event-card").forEach(card => {
|
||||||
card.addEventListener("click", () => {
|
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 id = Number(card.dataset.eventId);
|
||||||
const ev = events.find((e) => e.id === id);
|
const ev = events.find(e => e.id === id);
|
||||||
if (!ev) return;
|
if (!ev) return;
|
||||||
|
|
||||||
edpImg.src = ev.img;
|
edpImg.src = ev.img;
|
||||||
edpImg.alt = ev.label;
|
edpImg.alt = ev.label;
|
||||||
edpTitle.textContent = ev.label;
|
edpTitle.textContent = ev.label;
|
||||||
edpBody.textContent = "Inhalt folgt..."; // hier später befüllen
|
edpBody.textContent = "Inhalt folgt...";
|
||||||
|
|
||||||
overlay.classList.add("active");
|
overlay.classList.add("active");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Detail-Popup schließen */
|
/* ================================
|
||||||
body.querySelector("#edp-close-btn").addEventListener("click", () => {
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", e => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
overlay.classList.remove("active");
|
overlay.classList.remove("active");
|
||||||
});
|
eventsGrid.style.display = "";
|
||||||
|
boosterUi.style.display = "none";
|
||||||
overlay.addEventListener("click", (e) => {
|
clearAllIntervals();
|
||||||
if (e.target === overlay) overlay.classList.remove("active");
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
143
routes/booster.js
Normal file
143
routes/booster.js
Normal file
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user