This commit is contained in:
cay 2026-04-07 09:02:19 +01:00
parent 82803cf272
commit 42e5c19dd9
5 changed files with 353 additions and 132 deletions

View File

@ -375,6 +375,37 @@
cursor: not-allowed; cursor: not-allowed;
} }
.event-locked {
cursor: not-allowed;
opacity: 0.6;
}
.event-locked-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
border-radius: inherit;
pointer-events: none;
}
.event-locked-overlay span {
display: block;
padding: 4px 7px;
border: 2px solid #a05010;
border-radius: 5px;
color: #e08030;
font-family: "Cinzel", serif;
font-size: 9px;
font-weight: bold;
text-align: center;
line-height: 1.4;
background: rgba(0, 0, 0, 0.5);
white-space: nowrap;
}
.event-done-overlay { .event-done-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -395,7 +426,7 @@
border-radius: 5px; border-radius: 5px;
color: #3dbb3d; color: #3dbb3d;
font-family: "Cinzel", serif; font-family: "Cinzel", serif;
font-size: 14px; font-size: 11px;
font-weight: bold; font-weight: bold;
letter-spacing: 1px; letter-spacing: 1px;
text-transform: uppercase; text-transform: uppercase;

View File

@ -119,7 +119,7 @@
flex: 1; flex: 1;
font-size: 13px; font-size: 13px;
font-weight: bold; font-weight: bold;
color: #1a1008; color: #ffffff;
text-shadow: none; text-shadow: none;
} }

View File

@ -107,14 +107,17 @@ document.querySelectorAll(".qm-popup").forEach((popup) => {
================================ */ ================================ */
function isBoosterSpinning() { function isBoosterSpinning() {
// Nur sperren wenn das Events-Popup gerade offen UND der Slot dreht
const eventsPopup = document.getElementById("qm-popup-events"); const eventsPopup = document.getElementById("qm-popup-events");
if (!eventsPopup || !eventsPopup.classList.contains("active")) return false; if (!eventsPopup || !eventsPopup.classList.contains("active")) return false;
const ui = document.getElementById("booster-ui"); // Booster-UI oder Wood-UI aktiv und Slot dreht
if (!ui || ui.style.display === "none") return false; const boosterUi = document.getElementById("booster-ui");
if (boosterUi && boosterUi.style.display !== "none" && boosterUi.querySelector(".booster-slot.spinning")) return true;
return !!ui.querySelector(".booster-slot.spinning"); const woodUi = document.getElementById("wood-ui");
if (woodUi && woodUi.style.display !== "none" && woodUi.querySelector(".booster-slot.spinning")) return true;
return false;
} }
document.querySelectorAll(".qm-popup-close").forEach((btn) => { document.querySelectorAll(".qm-popup-close").forEach((btn) => {

View File

@ -20,22 +20,19 @@ function rarityImgs(rarity, size = 13) {
} }
function cardHTML(card, isFront = true) { function cardHTML(card, isFront = true) {
if (!isFront) if (!isFront) return `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
return `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
// image zuerst, dann icon als Fallback // image zuerst, dann icon als Fallback
const imgFile = card?.image || card?.icon || null; const imgFile = card?.image || card?.icon || null;
const img = imgFile const img = imgFile ? `/images/cards/${imgFile}` : "/images/items/rueckseite.png";
? `/images/cards/${imgFile}`
: "/images/items/rueckseite.png";
return ` return `
<img class="booster-slot-img" src="${img}" alt="${card?.name || ""}" draggable="false" <img class="booster-slot-img" src="${img}" alt="${card?.name || ''}" draggable="false"
onerror="this.src='/images/items/rueckseite.png'"> onerror="this.src='/images/items/rueckseite.png'">
${card?.attack != null ? `<span class="bs-stat-atk">${card.attack}</span>` : ""} ${card?.attack != null ? `<span class="bs-stat-atk">${card.attack}</span>` : ""}
${card?.defends != null ? `<span class="bs-stat-def">${card.defends}</span>` : ""} ${card?.defends != null ? `<span class="bs-stat-def">${card.defends}</span>` : ""}
${card?.cooldown != null ? `<span class="bs-stat-cd">${card.cooldown}</span>` : ""} ${card?.cooldown!= null ? `<span class="bs-stat-cd">${card.cooldown}</span>` : ""}
${card?.rarity ? `<div class="bs-rarity">${rarityImgs(card.rarity, 11)}</div>` : ""} ${card?.rarity ? `<div class="bs-rarity">${rarityImgs(card.rarity, 11)}</div>` : ""}
<div class="bs-card-name">${card?.name || ""}</div> <div class="bs-card-name">${card?.name || ''}</div>
`; `;
} }
@ -54,46 +51,43 @@ export async function loadEvents() {
} }
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: "Booster Öffnen", { id: 4, img: "/images/items/holz.png", label: "Holz Spenden", type: "wood", woodCost: 100 },
type: "booster", { id: 5, img: "/images/items/runenhaufen.png", label: "Textzeile 5" },
},
{ id: 2, img: "/images/items/1v1.png", label: "1 v 1" },
{ id: 3, img: "/images/items/2v2.png", label: "2 v 2" },
{ id: 4, img: "/images/items/holz.png", label: "Holz spenden" },
{ id: 5, img: "/images/items/goldmuenze.png", label: "Gold spenden" },
]; ];
// Täglichen Status vom Server laden // Täglichen Status + Holz-Bestand parallel laden
let completedToday = []; let completedToday = [];
let playerWood = 0;
try { try {
const statusRes = await fetch("/api/daily/status"); const [statusRes, hudRes] = await Promise.all([
if (statusRes.ok) { fetch("/api/daily/status"),
const statusData = await statusRes.json(); fetch("/api/hud"),
completedToday = statusData.completed || []; ]);
} if (statusRes.ok) completedToday = (await statusRes.json()).completed || [];
if (hudRes.ok) playerWood = (await hudRes.json()).wood || 0;
} catch (e) { } catch (e) {
console.error("Daily-Status laden fehlgeschlagen", e); console.error("Laden fehlgeschlagen", e);
} }
body.innerHTML = ` body.innerHTML = `
<div class="events-grid" id="events-grid"> <div class="events-grid" id="events-grid">
${events ${events.map(ev => {
.map((ev) => {
const done = completedToday.includes(ev.id); const done = completedToday.includes(ev.id);
const locked = !done && ev.woodCost && playerWood < ev.woodCost;
const cls = done ? ' event-done' : locked ? ' event-locked' : '';
return ` return `
<div class="event-card${done ? " event-done" : ""}" data-event-id="${ev.id}" data-type="${ev.type || ""}" data-done="${done}"> <div class="event-card${cls}" data-event-id="${ev.id}" data-type="${ev.type || ''}" data-done="${done}" data-locked="${locked}">
<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">
${done ? `<div class="event-done-overlay">✓</div>` : ""} ${done ? `<div class="event-done-overlay"></div>` : ''}
${locked ? `<div class="event-locked-overlay"><span>🪵 ${ev.woodCost} Holz<br>benötigt</span></div>` : ''}
</div> </div>
<span class="event-card-label">${ev.label}</span> <span class="event-card-label">${ev.label}</span>
${done ? `<span class="event-done-label">Bereits erledigt</span>` : ""}
</div>`; </div>`;
}) }).join("")}
.join("")}
</div> </div>
<!-- Booster UI --> <!-- Booster UI -->
@ -105,15 +99,31 @@ export async function loadEvents() {
<span class="booster-stapel-hint" id="booster-hint">Klicken zum Öffnen</span> <span class="booster-stapel-hint" id="booster-hint">Klicken zum Öffnen</span>
</div> </div>
<div class="booster-slots" id="booster-slots"> <div class="booster-slots" id="booster-slots">
${Array.from( ${Array.from({length: 5}, (_, i) => `
{ length: 5 },
(_, i) => `
<div class="booster-slot" id="booster-slot-${i}"> <div class="booster-slot" id="booster-slot-${i}">
<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>`, </div>`).join("")}
).join("")} </div>
</div>
</div>
<!-- Holz-Spenden UI -->
<div id="wood-ui" class="booster-ui" style="display:none;">
<button class="booster-back-btn" id="wood-back-btn"> Zurück</button>
<div class="booster-stage">
<div class="booster-left">
<img id="wood-btn" src="/images/items/holz.png" alt="Holz Spenden" draggable="false" class="booster-stapel-img">
<span class="booster-stapel-hint" id="wood-hint">100 Holz spenden</span>
</div>
<div class="booster-slots" id="wood-slots">
${Array.from({length: 5}, (_, i) => `
<div class="booster-slot" id="wood-slot-${i}">
<div class="booster-slot-inner">
<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">
</div>
</div>`).join("")}
</div> </div>
</div> </div>
</div> </div>
@ -134,13 +144,14 @@ export async function loadEvents() {
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 boosterUi = body.querySelector("#booster-ui");
const woodUi = body.querySelector("#wood-ui");
const eventsGrid = body.querySelector("#events-grid"); const eventsGrid = body.querySelector("#events-grid");
/* ── Event-Karten ── */ /* ── Event-Karten ── */
body.querySelectorAll(".event-card").forEach((card) => { body.querySelectorAll(".event-card").forEach(card => {
card.addEventListener("click", () => { card.addEventListener("click", () => {
// Bereits erledigt → nicht anklickbar
if (card.dataset.done === "true") return; if (card.dataset.done === "true") return;
if (card.dataset.locked === "true") return;
if (card.dataset.type === "booster") { if (card.dataset.type === "booster") {
eventsGrid.style.display = "none"; eventsGrid.style.display = "none";
@ -148,8 +159,14 @@ export async function loadEvents() {
resetBooster(); resetBooster();
return; return;
} }
if (card.dataset.type === "wood") {
eventsGrid.style.display = "none";
woodUi.style.display = "flex";
resetWood();
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;
@ -168,24 +185,16 @@ export async function loadEvents() {
body: JSON.stringify({ eventId }), body: JSON.stringify({ eventId }),
}); });
// Karte visuell als erledigt markieren // Karte visuell als erledigt markieren
const card = body.querySelector( const card = body.querySelector(`.event-card[data-event-id="${eventId}"]`);
`.event-card[data-event-id="${eventId}"]`,
);
if (card) { if (card) {
card.dataset.done = "true"; card.dataset.done = "true";
card.classList.add("event-done"); card.classList.add("event-done");
const wrap = card.querySelector(".event-card-img-wrap"); const wrap = card.querySelector(".event-card-img-wrap");
if (wrap && !wrap.querySelector(".event-done-overlay")) { if (wrap && !wrap.querySelector(".event-done-overlay")) {
wrap.insertAdjacentHTML( wrap.insertAdjacentHTML("beforeend", `<div class="event-done-overlay">✓</div>`);
"beforeend",
`<div class="event-done-overlay">✓</div>`,
);
} }
if (!card.querySelector(".event-done-label")) { if (!card.querySelector(".event-done-label")) {
card.insertAdjacentHTML( card.insertAdjacentHTML("beforeend", `<span class="event-done-label">Bereits erledigt</span>`);
"beforeend",
`<span class="event-done-label">Bereits erledigt</span>`,
);
} }
} }
} catch (e) { } catch (e) {
@ -193,17 +202,11 @@ export async function loadEvents() {
} }
} }
body body.querySelector("#edp-close-btn").addEventListener("click", () => overlay.classList.remove("active"));
.querySelector("#edp-close-btn") overlay.addEventListener("click", e => { if (e.target === overlay) overlay.classList.remove("active"); });
.addEventListener("click", () => overlay.classList.remove("active"));
overlay.addEventListener("click", (e) => {
if (e.target === overlay) overlay.classList.remove("active");
});
body.querySelector("#booster-back-btn").addEventListener("click", () => { body.querySelector("#booster-back-btn").addEventListener("click", () => {
// Gesperrt nur wenn Slot gerade läuft
if (isSpinning) return; if (isSpinning) return;
// Erlaubt wenn: Booster-Daily bereits erledigt ODER alle Karten enthüllt
const boosterCard = body.querySelector('.event-card[data-type="booster"]'); const boosterCard = body.querySelector('.event-card[data-type="booster"]');
const boosterDone = boosterCard?.dataset.done === "true"; const boosterDone = boosterCard?.dataset.done === "true";
if (!allRevealed && !boosterDone) return; if (!allRevealed && !boosterDone) return;
@ -213,6 +216,17 @@ export async function loadEvents() {
isSpinning = false; isSpinning = false;
}); });
body.querySelector("#wood-back-btn").addEventListener("click", () => {
if (isWoodSpinning) return;
const woodCard = body.querySelector('.event-card[data-type="wood"]');
const woodDone = woodCard?.dataset.done === "true";
if (!woodRevealed && !woodDone) return;
eventsGrid.style.display = "";
woodUi.style.display = "none";
clearWoodIntervals();
isWoodSpinning = false;
});
// KEIN document.addEventListener("keydown") hier // KEIN document.addEventListener("keydown") hier
// ESC wird zentral in quickmenu.js behandelt (verhindert Listener-Stapelung) // ESC wird zentral in quickmenu.js behandelt (verhindert Listener-Stapelung)
@ -224,7 +238,7 @@ export async function loadEvents() {
let slotLocked = {}; // { index: true } → verhindert Überschreiben nach reveal let slotLocked = {}; // { index: true } → verhindert Überschreiben nach reveal
function clearAllIntervals() { function clearAllIntervals() {
Object.values(spinIntervals).forEach((id) => clearInterval(id)); Object.values(spinIntervals).forEach(id => clearInterval(id));
spinIntervals = {}; spinIntervals = {};
slotLocked = {}; slotLocked = {};
} }
@ -246,13 +260,9 @@ export async function loadEvents() {
allRevealed = false; allRevealed = false;
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const inner = body.querySelector( const inner = body.querySelector(`#booster-slot-${i} .booster-slot-inner`);
`#booster-slot-${i} .booster-slot-inner`,
);
inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`; inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
body body.querySelector(`#booster-slot-${i}`).classList.remove("revealed", "spinning");
.querySelector(`#booster-slot-${i}`)
.classList.remove("revealed", "spinning");
} }
const stapel = body.querySelector("#booster-stapel"); const stapel = body.querySelector("#booster-stapel");
@ -341,15 +351,13 @@ export async function loadEvents() {
setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000); setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000);
} }
setTimeout( setTimeout(async () => {
async () => { body.querySelector("#booster-hint").textContent = "Karten werden gespeichert...";
body.querySelector("#booster-hint").textContent =
"Karten werden gespeichert...";
isSpinning = false; isSpinning = false;
// Karten in user_cards speichern // Karten in user_cards speichern
try { try {
const cardIds = drawnCards.map((c) => c.id); const cardIds = drawnCards.map(c => c.id);
const saveRes = await fetch("/api/booster/save", { const saveRes = await fetch("/api/booster/save", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -364,15 +372,149 @@ export async function loadEvents() {
await markDailyComplete(1); await markDailyComplete(1);
allRevealed = true; allRevealed = true;
body.querySelector("#booster-hint").textContent = body.querySelector("#booster-hint").textContent = "Alle Karten erhalten! ✓";
"Alle Karten erhalten! ✓";
const backBtn = body.querySelector("#booster-back-btn"); const backBtn = body.querySelector("#booster-back-btn");
backBtn.style.opacity = "1"; backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer"; backBtn.style.cursor = "pointer";
}, }, 5 * 5000 + 500);
5 * 5000 + 500,
);
}); });
preloadCards(); preloadCards();
/* ================================
Holz-Spenden Zustand
================================ */
let isWoodSpinning = false;
let woodRevealed = false;
let woodIntervals = {};
let woodLocked = {};
let rarity3Cards = [];
async function preloadRarity3() {
if (rarity3Cards.length) return;
try {
const res = await fetch("/api/booster/cards");
if (!res.ok) throw new Error(res.status);
const all = await res.json();
rarity3Cards = all.filter(c => parseInt(c.rarity) === 3);
// Fallback: wenn keine rarity-3 vorhanden alle nehmen
if (!rarity3Cards.length) rarity3Cards = all;
} catch (e) {
console.error("Rarity-3 Karten laden fehlgeschlagen", e);
}
}
function clearWoodIntervals() {
Object.values(woodIntervals).forEach(id => clearInterval(id));
woodIntervals = {};
woodLocked = {};
}
function resetWood() {
clearWoodIntervals();
isWoodSpinning = false;
woodRevealed = false;
for (let i = 0; i < 5; i++) {
const inner = body.querySelector(`#wood-slot-${i} .booster-slot-inner`);
inner.innerHTML = `<img class="booster-slot-img" src="/images/items/rueckseite.png" alt="?" draggable="false">`;
body.querySelector(`#wood-slot-${i}`).classList.remove("revealed", "spinning");
}
const btn = body.querySelector("#wood-btn");
btn.classList.remove("used");
btn.style.opacity = "1";
btn.style.cursor = "pointer";
body.querySelector("#wood-hint").textContent = "100 Holz spenden";
preloadRarity3();
}
function startWoodSlot(index) {
woodLocked[index] = false;
const slot = body.querySelector(`#wood-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.add("spinning");
const iv = setInterval(() => {
if (!rarity3Cards.length || woodLocked[index]) return;
const rnd = rarity3Cards[Math.floor(Math.random() * rarity3Cards.length)];
inner.innerHTML = cardHTML(rnd, true);
}, 150);
woodIntervals[index] = iv;
}
function revealWoodSlot(index, card) {
woodLocked[index] = true;
clearInterval(woodIntervals[index]);
delete woodIntervals[index];
const slot = body.querySelector(`#wood-slot-${index}`);
const inner = slot.querySelector(".booster-slot-inner");
slot.classList.remove("spinning");
slot.classList.add("revealed");
inner.innerHTML = cardHTML(card, true);
setTimeout(() => {
if (slot.classList.contains("revealed")) inner.innerHTML = cardHTML(card, true);
}, 100);
}
body.querySelector("#wood-btn").addEventListener("click", async () => {
if (isWoodSpinning) return;
if (!rarity3Cards.length) await preloadRarity3();
if (!rarity3Cards.length) return;
isWoodSpinning = true;
const btn = body.querySelector("#wood-btn");
btn.classList.add("used");
btn.style.opacity = "0.35";
btn.style.cursor = "default";
body.querySelector("#wood-hint").textContent = "Wird verarbeitet...";
const backBtn = body.querySelector("#wood-back-btn");
backBtn.style.opacity = "0.35";
backBtn.style.cursor = "not-allowed";
// API aufrufen 100 Holz abziehen + Karte ziehen
let drawnCard = null;
try {
const res = await fetch("/api/booster/wood-donate", { method: "POST" });
const data = await res.json();
if (!res.ok) {
body.querySelector("#wood-hint").textContent = data.error || "Fehler";
isWoodSpinning = false;
btn.classList.remove("used");
btn.style.opacity = "1";
btn.style.cursor = "pointer";
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
return;
}
drawnCard = data.card;
} catch (e) {
console.error("Holz-Spenden fehlgeschlagen", e);
resetWood();
return;
}
// Alle 5 Slots starten
for (let i = 0; i < 5; i++) startWoodSlot(i);
// Nach 5 Sekunden alle gleichzeitig enthüllen (dieselbe Karte)
setTimeout(() => {
for (let i = 0; i < 5; i++) revealWoodSlot(i, drawnCard);
body.querySelector("#wood-hint").textContent = "Karte erhalten! ✓";
isWoodSpinning = false;
woodRevealed = true;
markDailyComplete(4);
backBtn.style.opacity = "1";
backBtn.style.cursor = "pointer";
}, 5000);
});
preloadRarity3();
} }

View File

@ -143,4 +143,49 @@ router.post("/booster/save", async (req, res) => {
} }
}); });
/* ================================
POST /api/booster/wood-donate
100 Holz bezahlen 1 zufällige Rarity-3 Karte
================================ */
router.post("/booster/wood-donate", async (req, res) => {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
const userId = req.session.user.id;
try {
// Holz prüfen
const [[currency]] = await db.query(
"SELECT wood FROM account_currency WHERE account_id = ?", [userId]
);
if (!currency || currency.wood < 100) {
return res.status(400).json({ error: "Nicht genug Holz (100 benötigt)" });
}
// Rarity-3 Karten laden
const [pool] = await db.query(
"SELECT id, name, image, icon, max_level, rarity, attack, defends, cooldown FROM cards WHERE rarity = 3"
);
if (!pool.length) return res.status(400).json({ error: "Keine Rarity-3 Karten verfügbar" });
// 100 Holz abziehen
await db.query(
"UPDATE account_currency SET wood = wood - 100 WHERE account_id = ?", [userId]
);
// 1 zufällige Rarity-3 Karte ziehen
const card = pool[Math.floor(Math.random() * pool.length)];
// Direkt in user_cards speichern
await db.query(
`INSERT INTO user_cards (user_id, card_id, amount) VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE amount = amount + 1`,
[userId, card.id]
);
res.json({ card });
} catch (err) {
console.error(err);
res.status(500).json({ error: "DB Fehler" });
}
});
module.exports = router; module.exports = router;