const CARDS_PER_PAGE = 18; let currentGroupId = null; let currentPage = 1; let currentDeckId = null; let deckCards = []; // [{card_id, amount}] let userCardsCache = []; // aktuelle Seite: [{card_id, amount, name, image, rarity, attack, defense}] let decks = []; // [{id, name}] /* ── Kristall-Mapping ───────────────────────── */ 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 = 14) { const file = RARITY_CRYSTALS[String(rarity)]; if (!file) return ""; const count = parseInt(rarity) || 0; const img = `Stufe ${rarity}`; return img.repeat(count); } /* ══════════════════════════════════════════════ INIT ══════════════════════════════════════════════ */ export async function loadCardDeck() { const body = document.getElementById("qm-body-carddeck"); if (!body) return; currentDeckId = null; deckCards = []; body.innerHTML = renderShell(); attachShellEvents(); try { const res = await fetch("/api/card-groups"); if (!res.ok) throw new Error("API Fehler"); const groups = await res.json(); renderTabs(groups); if (groups.length) { currentGroupId = groups[0].id; await Promise.all([loadUserCards(), loadDecks()]); } } catch (err) { console.error("Init Fehler:", err); } } /* ══════════════════════════════════════════════ SHELL HTML ══════════════════════════════════════════════ */ function renderShell() { return `
Meine Karten
Lade Karten...
Deck-Builder
Kein Deck ausgewählt.
`; } /* ══════════════════════════════════════════════ TABS ══════════════════════════════════════════════ */ function renderTabs(groups) { const container = document.getElementById("kd-tabs"); if (!container) return; container.innerHTML = groups .map((g, i) => { const color = g.color || "#6b4b2a"; return ` `; }) .join(""); container.querySelectorAll(".kd-tab").forEach((btn) => { btn.addEventListener("click", async () => { const color = btn.style.borderColor; container.querySelectorAll(".kd-tab").forEach((b) => { b.classList.remove("kd-tab-active"); b.style.background = ""; }); btn.classList.add("kd-tab-active"); btn.style.background = `${color}33`; currentGroupId = parseInt(btn.dataset.group); currentPage = 1; await loadUserCards(); }); }); } /* ══════════════════════════════════════════════ USER-KARTEN laden GET /api/user-cards?group_id=X&page=Y&limit=Z → { cards: [{card_id, level, amount, name, image, attack, defense}], totalPages, total } ══════════════════════════════════════════════ */ async function loadUserCards() { const grid = document.getElementById("kd-grid"); const pagination = document.getElementById("kd-pagination"); if (!grid) return; grid.innerHTML = `
Lade Karten...
`; pagination.innerHTML = ""; try { const res = await fetch( `/api/user-cards?group_id=${currentGroupId}&page=${currentPage}&limit=${CARDS_PER_PAGE}`, ); if (!res.ok) throw new Error("API Fehler"); const data = await res.json(); userCardsCache = data.cards || []; renderCollectionGrid(grid, userCardsCache); renderPagination(pagination, data.totalPages, data.total); } catch { grid.innerHTML = `
Keine Karten gefunden.
`; } } /* ══════════════════════════════════════════════ DECKS laden GET /api/decks → [{id, name, card_count}] ══════════════════════════════════════════════ */ async function loadDecks() { try { const res = await fetch("/api/decks"); if (!res.ok) throw new Error("API Fehler"); decks = await res.json(); renderDeckSelect(); } catch (err) { console.error("Decks Fehler:", err); } } function renderDeckSelect() { const sel = document.getElementById("kd-deck-select"); if (!sel) return; sel.innerHTML = `` + decks .map( (d) => ``, ) .join(""); sel.value = currentDeckId || ""; } /* ══════════════════════════════════════════════ DECK-KARTEN laden GET /api/decks/:id/cards → [{card_id, level, amount, name, image, attack, defense}] ══════════════════════════════════════════════ */ async function loadDeckCards(deckId) { const grid = document.getElementById("kd-deck-grid"); const info = document.getElementById("kd-deck-info"); if (!grid) return; grid.innerHTML = `
Lade Deck...
`; info.innerHTML = ""; try { const res = await fetch(`/api/decks/${deckId}/cards`); if (!res.ok) throw new Error("API Fehler"); deckCards = await res.json(); const total = deckCards.reduce((s, c) => s + c.amount, 0); const isFull = total >= 30; info.innerHTML = ` Karten im Deck: ${total} / 30`; renderDeckGrid(grid, deckCards); // Sammlung neu rendern damit Deck-Zähler aktualisiert werden renderCollectionGrid(document.getElementById("kd-grid"), userCardsCache); } catch { grid.innerHTML = `
Fehler beim Laden des Decks.
`; } } /* ══════════════════════════════════════════════ KARTE ZUM DECK HINZUFÜGEN POST /api/decks/:id/cards { card_id, level } ══════════════════════════════════════════════ */ async function addCardToDeck(card) { if (!currentDeckId) { showToast("Bitte zuerst ein Deck auswählen."); return; } const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0); if (totalInDeck >= 30) { showToast("Deck ist voll! (max. 30 Karten)"); return; } // Rarity > 5: max. 1 Exemplar im Deck if (parseInt(card.rarity) > 5) { const already = deckCards.find((c) => c.card_id === card.card_id); if (already) { showToast("Karten ab Rarity 6 dürfen nur 1x im Deck sein."); return; } } // Nicht mehr als vorhanden besitzt const owned = card.amount; const inDeck = getDeckCount(card.card_id); if (inDeck >= owned) { showToast("Du hast keine weiteren Exemplare dieser Karte."); return; } try { const res = await fetch(`/api/decks/${currentDeckId}/cards`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ card_id: card.card_id }), }); if (!res.ok) { const err = await res.json(); showToast(err.message || "Fehler beim Hinzufügen."); return; } await loadDeckCards(currentDeckId); } catch { showToast("Verbindungsfehler."); } } /* ══════════════════════════════════════════════ KARTE AUS DECK ENTFERNEN DELETE /api/decks/:id/cards { card_id, level } ══════════════════════════════════════════════ */ async function removeCardFromDeck(card) { try { const res = await fetch(`/api/decks/${currentDeckId}/cards`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ card_id: card.card_id }), }); if (!res.ok) { const err = await res.json(); showToast(err.message || "Fehler beim Entfernen."); return; } await loadDeckCards(currentDeckId); } catch { showToast("Verbindungsfehler."); } } /* ══════════════════════════════════════════════ NEUES DECK ERSTELLEN POST /api/decks { name } ══════════════════════════════════════════════ */ async function createDeck(name) { try { const res = await fetch("/api/decks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); if (!res.ok) { const err = await res.json(); showToast(err.message || "Fehler beim Erstellen."); return; } const deck = await res.json(); await loadDecks(); // Neues Deck direkt auswählen currentDeckId = deck.id; deckCards = []; renderDeckSelect(); // Deck-Grid leeren + Info setzen document.getElementById("kd-deck-grid").innerHTML = `
Deck ist leer.
Klicke links auf eine Karte um sie hinzuzufügen.
`; document.getElementById("kd-deck-info").innerHTML = `Karten im Deck:0 / 30`; renderCollectionGrid(document.getElementById("kd-grid"), userCardsCache); } catch { showToast("Verbindungsfehler."); } } /* ══════════════════════════════════════════════ RENDER: SAMMLUNGS-GRID ══════════════════════════════════════════════ */ function renderCollectionGrid(grid, cards) { if (!grid) return; if (!cards || !cards.length) { grid.innerHTML = `
Keine Karten in dieser Gruppe.
`; return; } const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0); grid.innerHTML = cards .map((c) => { const inDeck = getDeckCount(c.card_id); const maxed = isMaxedOut(c, inDeck, totalInDeck); return `
${c.name} ${c.attack != null ? `${c.attack}` : ""} ${c.defends != null ? `${c.defends}` : ""} ${c.cooldown != null ? `${c.cooldown}` : ""} ${c.rarity ? `
${rarityImgs(c.rarity, 13)}
` : ""}
`; }) .join(""); grid.querySelectorAll(".kd-card:not(.kd-card-maxed)").forEach((el) => { el.addEventListener("click", async () => { const card = cards.find((c) => c.card_id === parseInt(el.dataset.cardId)); if (card) await addCardToDeck(card); }); }); } /* ══════════════════════════════════════════════ RENDER: DECK-GRID ══════════════════════════════════════════════ */ function renderDeckGrid(grid, cards) { if (!cards || !cards.length) { grid.innerHTML = `
Deck ist leer.
Klicke links auf eine Karte um sie hinzuzufügen.
`; return; } // Owned-Anzahl aus userCardsCache auslesen grid.innerHTML = cards .map((c) => { const ownedEntry = userCardsCache.find((u) => u.card_id === c.card_id); const ownedAmt = ownedEntry ? ownedEntry.amount : "?"; return `
${c.name} ${c.attack != null ? `${c.attack}` : ""} ${c.defends != null ? `${c.defends}` : ""} ${c.cooldown != null ? `${c.cooldown}` : ""} ${c.rarity ? `
${rarityImgs(c.rarity, 14)}
` : ""}
`; }) .join(""); grid.querySelectorAll(".kd-deck-card").forEach((el) => { el.addEventListener("click", async () => { const card = cards.find((c) => c.card_id === parseInt(el.dataset.cardId)); if (card) await removeCardFromDeck(card); }); }); } /* ══════════════════════════════════════════════ PAGINATION ══════════════════════════════════════════════ */ function renderPagination(pagination, totalPages, total) { if (!totalPages || totalPages <= 1) return; pagination.innerHTML = ` ${Array.from({ length: totalPages }, (_, i) => i + 1) .map( (p) => ` `, ) .join("")} ${total} Karten`; pagination.querySelector("#kd-prev")?.addEventListener("click", async () => { if (currentPage > 1) { currentPage--; await loadUserCards(); } }); pagination.querySelector("#kd-next")?.addEventListener("click", async () => { if (currentPage < totalPages) { currentPage++; await loadUserCards(); } }); pagination.querySelectorAll("[data-page]").forEach((btn) => { btn.addEventListener("click", async () => { currentPage = parseInt(btn.dataset.page); await loadUserCards(); }); }); } /* ══════════════════════════════════════════════ EVENTS (Shell-Ebene) ══════════════════════════════════════════════ */ function attachShellEvents() { // Deck-Auswahl – direkt auf Element, kein document-Listener const sel = document.getElementById("kd-deck-select"); if (sel) { sel.addEventListener("change", async (e) => { const val = parseInt(e.target.value); if (!val) { currentDeckId = null; deckCards = []; document.getElementById("kd-deck-grid").innerHTML = `
Kein Deck ausgewählt.
`; document.getElementById("kd-deck-info").innerHTML = ""; renderCollectionGrid( document.getElementById("kd-grid"), userCardsCache, ); return; } currentDeckId = val; await loadDeckCards(currentDeckId); }); } // Neues Deck – direkt auf Button, kein document-Listener const btnNew = document.getElementById("kd-btn-new-deck"); if (btnNew) { btnNew.addEventListener("click", () => showNewDeckModal()); } } /* ══════════════════════════════════════════════ MODAL: Neues Deck ══════════════════════════════════════════════ */ function showNewDeckModal() { if (decks.length >= 10) { showToast("Du hast bereits 10 Decks (Maximum erreicht)."); return; } const wrap = document.querySelector(".kd-split"); const modal = document.createElement("div"); modal.className = "kd-modal-overlay"; modal.innerHTML = `

Neues Deck erstellen

`; wrap.appendChild(modal); const input = modal.querySelector("#kd-new-deck-name"); input.focus(); modal.querySelector("#kd-modal-cancel").onclick = () => modal.remove(); modal.querySelector("#kd-modal-confirm").onclick = async () => { const name = input.value.trim(); if (!name) { input.focus(); return; } modal.remove(); await createDeck(name); }; input.addEventListener("keydown", async (e) => { if (e.key === "Enter") { const name = input.value.trim(); if (!name) return; modal.remove(); await createDeck(name); } if (e.key === "Escape") modal.remove(); }); } /* ══════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════ */ function getDeckCount(cardId) { const entry = deckCards.find((c) => c.card_id === cardId); return entry ? entry.amount : 0; } function isMaxedOut(card, inDeck, totalInDeck) { if (!currentDeckId) return false; if (totalInDeck >= 30) return true; if (inDeck >= card.amount) return true; // mehr als besessen if (parseInt(card.rarity) > 5 && inDeck >= 1) return true; // Rarity > 5: nur 1x return false; } function showToast(msg) { const existing = document.querySelector(".kd-toast"); if (existing) existing.remove(); const t = document.createElement("div"); t.className = "kd-toast"; t.textContent = msg; t.style.cssText = ` position:fixed; bottom:80px; left:50%; transform:translateX(-50%); background:linear-gradient(#4a2808,#2a1004); border:2px solid #8b6a3c; border-radius:8px; color:#f0d9a6; font-family:"Cinzel",serif; font-size:13px; padding:10px 22px; z-index:9999; pointer-events:none; animation:kd-pulse 0.3s ease;`; document.body.appendChild(t); setTimeout(() => t.remove(), 2800); }