diff --git a/public/css/events.css b/public/css/events.css index f138ec5..3e50f2d 100644 --- a/public/css/events.css +++ b/public/css/events.css @@ -31,7 +31,7 @@ justify-content: center; padding: 6px; box-sizing: border-box; - position: relative; /* ← fix: damit event-done-overlay korrekt positioniert wird */ + position: relative; /* ← fix: damit event-done-overlay korrekt positioniert wird */ } .event-card-img-wrap img { @@ -375,6 +375,37 @@ 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 { position: absolute; inset: 0; @@ -395,7 +426,7 @@ border-radius: 5px; color: #3dbb3d; font-family: "Cinzel", serif; - font-size: 14px; + font-size: 11px; font-weight: bold; letter-spacing: 1px; text-transform: uppercase; diff --git a/public/css/mine.css b/public/css/mine.css index 785314d..2af1f60 100644 --- a/public/css/mine.css +++ b/public/css/mine.css @@ -119,7 +119,7 @@ flex: 1; font-size: 13px; font-weight: bold; - color: #1a1008; + color: #ffffff; text-shadow: none; } diff --git a/public/js/quickmenu.js b/public/js/quickmenu.js index 6702498..1a30304 100644 --- a/public/js/quickmenu.js +++ b/public/js/quickmenu.js @@ -107,14 +107,17 @@ document.querySelectorAll(".qm-popup").forEach((popup) => { ================================ */ function isBoosterSpinning() { - // Nur sperren wenn das Events-Popup gerade offen UND der Slot dreht const eventsPopup = document.getElementById("qm-popup-events"); if (!eventsPopup || !eventsPopup.classList.contains("active")) return false; - const ui = document.getElementById("booster-ui"); - if (!ui || ui.style.display === "none") return false; + // Booster-UI oder Wood-UI aktiv und Slot dreht + 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) => { diff --git a/public/js/quickmenu/events.js b/public/js/quickmenu/events.js index c7223ef..dc96ffb 100644 --- a/public/js/quickmenu/events.js +++ b/public/js/quickmenu/events.js @@ -20,22 +20,19 @@ function rarityImgs(rarity, size = 13) { } function cardHTML(card, isFront = true) { - if (!isFront) - return `?`; + if (!isFront) return `?`; // image zuerst, dann icon als Fallback const imgFile = card?.image || card?.icon || null; - const img = imgFile - ? `/images/cards/${imgFile}` - : "/images/items/rueckseite.png"; + const img = imgFile ? `/images/cards/${imgFile}` : "/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 || ""}
+ ${card?.attack != null ? `${card.attack}` : ""} + ${card?.defends != null ? `${card.defends}` : ""} + ${card?.cooldown!= null ? `${card.cooldown}` : ""} + ${card?.rarity ? `
${rarityImgs(card.rarity, 11)}
` : ""} +
${card?.name || ''}
`; } @@ -48,52 +45,49 @@ export async function loadEvents() { if (!document.querySelector('link[href="/css/events.css"]')) { const link = document.createElement("link"); - link.rel = "stylesheet"; + link.rel = "stylesheet"; link.href = "/css/events.css"; document.head.appendChild(link); } const events = [ - { - id: 1, - img: "/images/items/runenhaufen.png", - label: "Booster Öffnen", - type: "booster", - }, - { 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" }, + { 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/holz.png", label: "Holz Spenden", type: "wood", woodCost: 100 }, + { id: 5, img: "/images/items/runenhaufen.png", label: "Textzeile 5" }, ]; - // Täglichen Status vom Server laden + // Täglichen Status + Holz-Bestand parallel laden let completedToday = []; + let playerWood = 0; try { - const statusRes = await fetch("/api/daily/status"); - if (statusRes.ok) { - const statusData = await statusRes.json(); - completedToday = statusData.completed || []; - } + const [statusRes, hudRes] = await Promise.all([ + fetch("/api/daily/status"), + fetch("/api/hud"), + ]); + if (statusRes.ok) completedToday = (await statusRes.json()).completed || []; + if (hudRes.ok) playerWood = (await hudRes.json()).wood || 0; } catch (e) { - console.error("Daily-Status laden fehlgeschlagen", e); + console.error("Laden fehlgeschlagen", e); } body.innerHTML = `
- ${events - .map((ev) => { - const done = completedToday.includes(ev.id); - return ` -
+ ${events.map(ev => { + const done = completedToday.includes(ev.id); + const locked = !done && ev.woodCost && playerWood < ev.woodCost; + const cls = done ? ' event-done' : locked ? ' event-locked' : ''; + return ` +
${ev.label} - ${done ? `
` : ""} + ${done ? `
` : ''} + ${locked ? `
🪵 ${ev.woodCost} Holz
benötigt
` : ''}
${ev.label} - ${done ? `Bereits erledigt` : ""}
`; - }) - .join("")} + }).join("")}
@@ -105,15 +99,31 @@ export async function loadEvents() { Klicken zum Öffnen
- ${Array.from( - { length: 5 }, - (_, i) => ` + ${Array.from({length: 5}, (_, i) => `
?
-
`, - ).join("")} +
`).join("")} + + + + + + @@ -129,32 +139,39 @@ export async function loadEvents() { `; - 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 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 woodUi = body.querySelector("#wood-ui"); const eventsGrid = body.querySelector("#events-grid"); /* ── Event-Karten ── */ - body.querySelectorAll(".event-card").forEach((card) => { + body.querySelectorAll(".event-card").forEach(card => { 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") { eventsGrid.style.display = "none"; - boosterUi.style.display = "flex"; + boosterUi.style.display = "flex"; resetBooster(); return; } + if (card.dataset.type === "wood") { + eventsGrid.style.display = "none"; + woodUi.style.display = "flex"; + resetWood(); + 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..."; + edpBody.textContent = "Inhalt folgt..."; overlay.classList.add("active"); }); }); @@ -168,24 +185,16 @@ export async function loadEvents() { body: JSON.stringify({ eventId }), }); // Karte visuell als erledigt markieren - const card = body.querySelector( - `.event-card[data-event-id="${eventId}"]`, - ); + const card = body.querySelector(`.event-card[data-event-id="${eventId}"]`); if (card) { card.dataset.done = "true"; card.classList.add("event-done"); const wrap = card.querySelector(".event-card-img-wrap"); if (wrap && !wrap.querySelector(".event-done-overlay")) { - wrap.insertAdjacentHTML( - "beforeend", - `
`, - ); + wrap.insertAdjacentHTML("beforeend", `
`); } if (!card.querySelector(".event-done-label")) { - card.insertAdjacentHTML( - "beforeend", - `Bereits erledigt`, - ); + card.insertAdjacentHTML("beforeend", `Bereits erledigt`); } } } catch (e) { @@ -193,40 +202,45 @@ export async function loadEvents() { } } - body - .querySelector("#edp-close-btn") - .addEventListener("click", () => overlay.classList.remove("active")); - overlay.addEventListener("click", (e) => { - if (e.target === overlay) 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"); }); body.querySelector("#booster-back-btn").addEventListener("click", () => { - // Gesperrt nur wenn Slot gerade läuft if (isSpinning) return; - // Erlaubt wenn: Booster-Daily bereits erledigt ODER alle Karten enthüllt const boosterCard = body.querySelector('.event-card[data-type="booster"]'); const boosterDone = boosterCard?.dataset.done === "true"; if (!allRevealed && !boosterDone) return; eventsGrid.style.display = ""; - boosterUi.style.display = "none"; + boosterUi.style.display = "none"; clearAllIntervals(); 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 – // ESC wird zentral in quickmenu.js behandelt (verhindert Listener-Stapelung) /* ── Booster Zustand ── */ - let allCards = []; - let isSpinning = false; - let allRevealed = false; // true sobald alle 5 Karten aufgedeckt + gespeichert - let spinIntervals = {}; // { index: intervalId } - let slotLocked = {}; // { index: true } → verhindert Überschreiben nach reveal + let allCards = []; + let isSpinning = false; + let allRevealed = false; // true sobald alle 5 Karten aufgedeckt + gespeichert + let spinIntervals = {}; // { index: intervalId } + let slotLocked = {}; // { index: true } → verhindert Überschreiben nach reveal function clearAllIntervals() { - Object.values(spinIntervals).forEach((id) => clearInterval(id)); + Object.values(spinIntervals).forEach(id => clearInterval(id)); spinIntervals = {}; - slotLocked = {}; + slotLocked = {}; } async function preloadCards() { @@ -242,23 +256,19 @@ export async function loadEvents() { function resetBooster() { clearAllIntervals(); - isSpinning = false; + isSpinning = false; allRevealed = false; for (let i = 0; i < 5; i++) { - const inner = body.querySelector( - `#booster-slot-${i} .booster-slot-inner`, - ); + const inner = body.querySelector(`#booster-slot-${i} .booster-slot-inner`); inner.innerHTML = `?`; - body - .querySelector(`#booster-slot-${i}`) - .classList.remove("revealed", "spinning"); + 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"; + stapel.style.cursor = "pointer"; body.querySelector("#booster-hint").textContent = "Klicken zum Öffnen"; preloadCards(); } @@ -266,7 +276,7 @@ export async function loadEvents() { /* ── Slot drehen – 350ms, damit man die Karten erkennt ── */ function startSpinSlot(index) { slotLocked[index] = false; - const slot = body.querySelector(`#booster-slot-${index}`); + const slot = body.querySelector(`#booster-slot-${index}`); const inner = slot.querySelector(".booster-slot-inner"); slot.classList.add("spinning"); @@ -281,13 +291,13 @@ export async function loadEvents() { /* ── Slot enthüllen – Sperre setzen BEVOR clearInterval ── */ function revealSlot(index, card) { - slotLocked[index] = true; // zuerst sperren + slotLocked[index] = true; // zuerst sperren clearInterval(spinIntervals[index]); // dann stoppen delete spinIntervals[index]; console.log(`[Booster] Slot ${index} enthüllt:`, card); - const slot = body.querySelector(`#booster-slot-${index}`); + const slot = body.querySelector(`#booster-slot-${index}`); const inner = slot.querySelector(".booster-slot-inner"); slot.classList.remove("spinning"); slot.classList.add("revealed"); @@ -312,17 +322,17 @@ export async function loadEvents() { const stapel = body.querySelector("#booster-stapel"); stapel.classList.add("used"); stapel.style.opacity = "0.35"; - stapel.style.cursor = "default"; + stapel.style.cursor = "default"; body.querySelector("#booster-hint").textContent = "Wird gezogen..."; // Zurück-Button während der Animation sperren const backBtn = body.querySelector("#booster-back-btn"); backBtn.style.opacity = "0.35"; - backBtn.style.cursor = "not-allowed"; + backBtn.style.cursor = "not-allowed"; let drawnCards = []; try { - const res = await fetch("/api/booster/open", { method: "POST" }); + const res = await fetch("/api/booster/open", { method: "POST" }); const text = await res.text(); console.log("[Booster] API Antwort raw:", text); const data = JSON.parse(text); @@ -341,38 +351,170 @@ export async function loadEvents() { setTimeout(() => revealSlot(i, drawnCards[i]), (i + 1) * 5000); } - setTimeout( - async () => { - body.querySelector("#booster-hint").textContent = - "Karten werden gespeichert..."; - isSpinning = false; + setTimeout(async () => { + body.querySelector("#booster-hint").textContent = "Karten werden gespeichert..."; + isSpinning = false; - // Karten in user_cards speichern - try { - const cardIds = drawnCards.map((c) => c.id); - const saveRes = await fetch("/api/booster/save", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ cardIds }), - }); - if (!saveRes.ok) throw new Error(saveRes.status); - } catch (e) { - console.error("Karten speichern fehlgeschlagen", e); - } + // Karten in user_cards speichern + try { + const cardIds = drawnCards.map(c => c.id); + const saveRes = await fetch("/api/booster/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ cardIds }), + }); + if (!saveRes.ok) throw new Error(saveRes.status); + } catch (e) { + console.error("Karten speichern fehlgeschlagen", e); + } - // Booster-Daily als erledigt markieren (event_id = 1) - await markDailyComplete(1); + // Booster-Daily als erledigt markieren (event_id = 1) + await markDailyComplete(1); - allRevealed = true; - body.querySelector("#booster-hint").textContent = - "Alle Karten erhalten! ✓"; - const backBtn = body.querySelector("#booster-back-btn"); - backBtn.style.opacity = "1"; - backBtn.style.cursor = "pointer"; - }, - 5 * 5000 + 500, - ); + allRevealed = true; + body.querySelector("#booster-hint").textContent = "Alle Karten erhalten! ✓"; + const backBtn = body.querySelector("#booster-back-btn"); + backBtn.style.opacity = "1"; + backBtn.style.cursor = "pointer"; + }, 5 * 5000 + 500); }); 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 = `?`; + 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(); } diff --git a/routes/booster.js b/routes/booster.js index 4a23bd8..bad06a0 100644 --- a/routes/booster.js +++ b/routes/booster.js @@ -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;