diff --git a/app.js b/app.js index 1bd3471..8071216 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,7 @@ const equip = require("./routes/equip"); const equipment = require("./routes/equipment"); const blackmarket = require("./routes/blackmarket"); const mineRoute = require("./routes/mine"); +const carddeckRoutes = require("./routes/carddeck"); const arenaRoutes = require("./routes/arena"); const { registerArenaHandlers } = require("./sockets/arena"); const { registerChatHandlers } = require("./sockets/chat"); @@ -297,44 +298,9 @@ app.get("/api/buildings", requireLogin, async (req, res) => { } }); -/* ======================== - Card Groups API -======================== */ - -app.get("/api/card-groups", requireLogin, async (req, res) => { - try { - const [groups] = await db.query("SELECT * FROM card_groups ORDER BY id"); - res.json(groups); - } catch (err) { - console.error(err); - res.status(500).json({ error: "DB Fehler" }); } }); -/* ======================== - Cards API -======================== */ - -app.get("/api/cards", requireLogin, async (req, res) => { - const { group_id, page = 1, limit = 12 } = req.query; - const offset = (page - 1) * limit; - - try { - const [cards] = await db.query( - `SELECT c.*, cg.name AS group_name, cg.color AS group_color, - cl.attack, cl.defense, cl.cooldown - FROM cards c - LEFT JOIN card_groups cg ON cg.id = c.group_id - LEFT JOIN card_levels cl ON cl.card_id = c.id AND cl.level = 1 - WHERE c.group_id = ? - LIMIT ? OFFSET ?`, - [group_id, parseInt(limit), parseInt(offset)] - ); - const [[{ total }]] = await db.query( - "SELECT COUNT(*) as total FROM cards WHERE group_id = ?", [group_id] - ); - - res.json({ cards, total, page: parseInt(page), totalPages: Math.ceil(total / limit) }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); @@ -367,6 +333,7 @@ app.use("/api/equip", equip); app.use("/api/equipment", equipment); app.use("/api/blackmarket", blackmarket); app.use("/api/mine", mineRoute); +app.use("/api", carddeckRoutes); app.use("/arena", arenaRoutes); /* ======================== diff --git a/public/js/quickmenu/carddeck.js b/public/js/quickmenu/carddeck.js index 4601c39..0ca318d 100644 --- a/public/js/quickmenu/carddeck.js +++ b/public/js/quickmenu/carddeck.js @@ -1,254 +1,458 @@ -const CARDS_PER_PAGE = 12; +const CARDS_PER_PAGE = 18; -let currentGroupId = null; -let currentPage = 1; +let currentGroupId = null; +let currentPage = 1; +let currentDeckId = null; +let deckCards = []; // [{card_id, level, amount}] +let userCardsCache = []; // aktuelle Seite: [{card_id, level, amount, name, image, attack, defense}] +let decks = []; // [{id, name}] +/* ══════════════════════════════════════════════ + INIT +══════════════════════════════════════════════ */ export async function loadCardDeck() { const body = document.getElementById("qm-body-carddeck"); if (!body) return; 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 loadCards(); + await Promise.all([loadUserCards(), loadDecks()]); } } catch (err) { - console.error("Gruppen Fehler:", err); + console.error("Init Fehler:", err); } } -/* ── Grundstruktur ──────────────────────────── */ +/* ══════════════════════════════════════════════ + SHELL HTML +══════════════════════════════════════════════ */ function renderShell() { return `
+ + -
-
-
-
+ + +
+ + +
+
Meine Karten
+
+
Lade Karten...
+
+
+
+ + +
+
Deck-Builder
+ + +
+ + +
+ + +
+ + +
+
Kein Deck ausgewählt.
+
+
+ +
`; } -/* ── Tabs aus DB ────────────────────────────── */ +/* ══════════════════════════════════════════════ + TABS +══════════════════════════════════════════════ */ function renderTabs(groups) { const container = document.getElementById("kd-tabs"); if (!container) return; @@ -261,8 +465,7 @@ function renderTabs(groups) { style="border-color:${color};${i === 0 ? `background:${color}33;` : ""}"> ${g.name} - - `; + `; }).join(""); container.querySelectorAll(".kd-tab").forEach((btn) => { @@ -275,14 +478,19 @@ function renderTabs(groups) { btn.classList.add("kd-tab-active"); btn.style.background = `${color}33`; currentGroupId = parseInt(btn.dataset.group); - currentPage = 1; - await loadCards(); + currentPage = 1; + await loadUserCards(); }); }); } -/* ── Karten laden ───────────────────────────── */ -async function loadCards() { +/* ══════════════════════════════════════════════ + 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; @@ -291,54 +499,371 @@ async function loadCards() { pagination.innerHTML = ""; try { - const res = await fetch(`/api/cards?group_id=${currentGroupId}&page=${currentPage}&limit=${CARDS_PER_PAGE}`); + 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(); - renderGrid(grid, data.cards); + userCardsCache = data.cards || []; + renderCollectionGrid(grid, userCardsCache); renderPagination(pagination, data.totalPages, data.total); } catch { - grid.innerHTML = `
Noch keine Karten für diese Gruppe vorhanden.
`; + grid.innerHTML = `
Keine Karten gefunden.
`; } } -/* ── Grid ───────────────────────────────────── */ -function renderGrid(grid, cards) { - if (!cards || !cards.length) { - grid.innerHTML = `
Noch keine Karten für diese Gruppe vorhanden.
`; +/* ══════════════════════════════════════════════ + 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(""); +} + +/* ══════════════════════════════════════════════ + 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; } - grid.innerHTML = cards.map((c) => ` -
- ${c.name} - ${c.attack != null ? `${c.attack}` : ""} - ${c.defense != null ? `${c.defense}` : ""} -
${c.name}
-
- `).join(""); + + const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0); + if (totalInDeck >= 30) { + showToast("Deck ist voll! (max. 30 Karten)"); + return; + } + + // Level > 5: max. 1 Exemplar im Deck + if (card.level > 5) { + const already = deckCards.find(c => c.card_id === card.card_id && c.level === card.level); + if (already) { + showToast("Karten ab Level 6 dürfen nur 1x im Deck sein."); + return; + } + } + + // Nicht mehr als vorhanden besitzt + const owned = card.amount; + const inDeck = getDeckCount(card.card_id, card.level); + 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, level: card.level }), + }); + if (!res.ok) { + const err = await res.json(); + showToast(err.message || "Fehler beim Hinzufügen."); + return; + } + await loadDeckCards(currentDeckId); + } catch { + showToast("Verbindungsfehler."); + } } -/* ── Pagination ─────────────────────────────── */ -function renderPagination(pagination, totalPages, total) { - if (totalPages <= 1) return; - pagination.innerHTML = ` - - ${Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ` - - `).join("")} - - ${total} Karten - `; - document.getElementById("kd-prev")?.addEventListener("click", async () => { - if (currentPage > 1) { currentPage--; await loadCards(); } - }); - document.getElementById("kd-next")?.addEventListener("click", async () => { - if (currentPage < totalPages) { currentPage++; await loadCards(); } - }); - pagination.querySelectorAll("[data-page]").forEach((btn) => { - btn.addEventListener("click", async () => { - currentPage = parseInt(btn.dataset.page); - await loadCards(); +/* ══════════════════════════════════════════════ + 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, level: card.level }), + }); + 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(); + currentDeckId = deck.id; + renderDeckSelect(); + deckCards = []; + 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, c.level); + const maxed = isMaxedOut(c, inDeck, totalInDeck); + return ` +
+
${c.name}
+ ${c.name} + ${c.attack != null ? `${c.attack}` : ""} + ${c.defense != null ? `${c.defense}` : ""} + +
`; + }).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) && c.level === parseInt(el.dataset.level) + ); + 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; + } + grid.innerHTML = cards.map(c => ` +
+ ${c.amount > 1 ? `${c.amount}×` : ""} + ${c.name} + +
`).join(""); + + grid.querySelectorAll(".kd-deck-card").forEach(el => { + el.addEventListener("click", async () => { + const card = cards.find( + c => c.card_id === parseInt(el.dataset.cardId) && c.level === parseInt(el.dataset.level) + ); + 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 + document.addEventListener("change", async (e) => { + if (e.target.id !== "kd-deck-select") return; + 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 + document.addEventListener("click", (e) => { + if (e.target.id !== "kd-btn-new-deck") return; + 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, level) { + const entry = deckCards.find(c => c.card_id === cardId && c.level === level); + 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 (card.level > 5 && inDeck >= 1) return true; // Level > 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); +} diff --git a/routes/carddeck.route.js b/routes/carddeck.route.js new file mode 100644 index 0000000..d4c907d --- /dev/null +++ b/routes/carddeck.route.js @@ -0,0 +1,326 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../database/database"); + +/* ======================== + Auth Middleware +======================== */ + +function requireLogin(req, res, next) { + if (!req.session.user) return res.status(401).json({ error: "Nicht eingeloggt" }); + next(); +} + +router.use(requireLogin); + +/* ════════════════════════════════════════════ + GET /api/card-groups + Alle Kartengruppen +════════════════════════════════════════════ */ + +router.get("/card-groups", async (req, res) => { + try { + const [groups] = await db.query("SELECT * FROM card_groups ORDER BY id"); + res.json(groups); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + GET /api/cards?group_id=X&page=Y&limit=Z + Alle Karten (Admin-/Gruppen-Ansicht, kein Besitz) +════════════════════════════════════════════ */ + +router.get("/cards", async (req, res) => { + const { group_id, page = 1, limit = 12 } = req.query; + const offset = (page - 1) * limit; + + try { + const [cards] = await db.query( + `SELECT c.*, cg.name AS group_name, cg.color AS group_color, + cl.attack, cl.defense, cl.cooldown + FROM cards c + LEFT JOIN card_groups cg ON cg.id = c.group_id + LEFT JOIN card_levels cl ON cl.card_id = c.id AND cl.level = 1 + WHERE c.group_id = ? + LIMIT ? OFFSET ?`, + [group_id, parseInt(limit), parseInt(offset)] + ); + const [[{ total }]] = await db.query( + "SELECT COUNT(*) AS total FROM cards WHERE group_id = ?", + [group_id] + ); + res.json({ cards, total, page: parseInt(page), totalPages: Math.ceil(total / limit) }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + GET /api/user-cards?group_id=X&page=Y&limit=Z + Karten die der Spieler besitzt (mit amount) +════════════════════════════════════════════ */ + +router.get("/user-cards", async (req, res) => { + const userId = req.session.user.id; + const { group_id, page = 1, limit = 18 } = req.query; + const offset = (page - 1) * limit; + + try { + const [cards] = await db.query( + `SELECT + uc.card_id, + uc.level, + uc.amount, + c.name, + c.image, + cg.name AS group_name, + cg.color AS group_color, + cl.attack, + cl.defense, + cl.cooldown + FROM user_cards uc + JOIN cards c ON c.id = uc.card_id + JOIN card_groups cg ON cg.id = c.group_id + LEFT JOIN card_levels cl ON cl.card_id = uc.card_id AND cl.level = uc.level + WHERE uc.user_id = ? AND c.group_id = ? + ORDER BY c.id, uc.level + LIMIT ? OFFSET ?`, + [userId, group_id, parseInt(limit), parseInt(offset)] + ); + + const [[{ total }]] = await db.query( + `SELECT COUNT(*) AS total + FROM user_cards uc + JOIN cards c ON c.id = uc.card_id + WHERE uc.user_id = ? AND c.group_id = ?`, + [userId, group_id] + ); + + res.json({ cards, total, page: parseInt(page), totalPages: Math.ceil(total / limit) }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + GET /api/decks + Alle Decks des Spielers +════════════════════════════════════════════ */ + +router.get("/decks", async (req, res) => { + const userId = req.session.user.id; + try { + const [decks] = await db.query( + `SELECT d.id, d.name, + COALESCE(SUM(dc.amount), 0) AS card_count + FROM decks d + LEFT JOIN deck_cards dc ON dc.deck_id = d.id + WHERE d.user_id = ? + GROUP BY d.id + ORDER BY d.created_at`, + [userId] + ); + res.json(decks); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + POST /api/decks { name } + Neues Deck erstellen (max. 10 pro Spieler) +════════════════════════════════════════════ */ + +router.post("/decks", async (req, res) => { + const userId = req.session.user.id; + const { name } = req.body; + + if (!name || !name.trim()) { + return res.status(400).json({ error: "Name darf nicht leer sein." }); + } + + try { + const [[{ count }]] = await db.query( + "SELECT COUNT(*) AS count FROM decks WHERE user_id = ?", + [userId] + ); + if (count >= 10) { + return res.status(400).json({ error: "Maximale Anzahl von 10 Decks erreicht." }); + } + + const [result] = await db.query( + "INSERT INTO decks (user_id, name) VALUES (?, ?)", + [userId, name.trim()] + ); + res.status(201).json({ id: result.insertId, name: name.trim(), card_count: 0 }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + GET /api/decks/:id/cards + Karten eines bestimmten Decks +════════════════════════════════════════════ */ + +router.get("/decks/:id/cards", async (req, res) => { + const userId = req.session.user.id; + const deckId = req.params.id; + + try { + // Deck gehört dem Spieler? + const [[deck]] = await db.query( + "SELECT id FROM decks WHERE id = ? AND user_id = ?", + [deckId, userId] + ); + if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." }); + + const [cards] = await db.query( + `SELECT + dc.card_id, + dc.level, + dc.amount, + c.name, + c.image, + cl.attack, + cl.defense + FROM deck_cards dc + JOIN cards c ON c.id = dc.card_id + LEFT JOIN card_levels cl ON cl.card_id = dc.card_id AND cl.level = dc.level + WHERE dc.deck_id = ? + ORDER BY c.name, dc.level`, + [deckId] + ); + res.json(cards); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + POST /api/decks/:id/cards { card_id, level } + Karte zum Deck hinzufügen +════════════════════════════════════════════ */ + +router.post("/decks/:id/cards", async (req, res) => { + const userId = req.session.user.id; + const deckId = req.params.id; + const { card_id, level = 1 } = req.body; + + if (!card_id) return res.status(400).json({ error: "card_id fehlt." }); + + try { + // Deck gehört dem Spieler? + const [[deck]] = await db.query( + "SELECT id FROM decks WHERE id = ? AND user_id = ?", + [deckId, userId] + ); + if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." }); + + // Besitzt der Spieler diese Karte mit diesem Level? + const [[owned]] = await db.query( + "SELECT amount FROM user_cards WHERE user_id = ? AND card_id = ? AND level = ?", + [userId, card_id, level] + ); + if (!owned) return res.status(400).json({ error: "Du besitzt diese Karte nicht." }); + + // Deck-Gesamtzahl prüfen (max 30) + const [[{ total }]] = await db.query( + "SELECT COALESCE(SUM(amount), 0) AS total FROM deck_cards WHERE deck_id = ?", + [deckId] + ); + if (total >= 30) { + return res.status(400).json({ error: "Deck ist voll (max. 30 Karten)." }); + } + + // Level > 5: max. 1× im Deck + const [[existing]] = await db.query( + "SELECT amount FROM deck_cards WHERE deck_id = ? AND card_id = ? AND level = ?", + [deckId, card_id, level] + ); + + if (level > 5 && existing) { + return res.status(400).json({ error: "Karten ab Level 6 dürfen nur einmal im Deck sein." }); + } + + // Nicht mehr einfügen als besessen + const currentInDeck = existing ? existing.amount : 0; + if (currentInDeck >= owned.amount) { + return res.status(400).json({ error: "Du hast keine weiteren Exemplare dieser Karte." }); + } + + // Einfügen oder erhöhen + if (existing) { + await db.query( + "UPDATE deck_cards SET amount = amount + 1 WHERE deck_id = ? AND card_id = ? AND level = ?", + [deckId, card_id, level] + ); + } else { + await db.query( + "INSERT INTO deck_cards (deck_id, card_id, level, amount) VALUES (?, ?, ?, 1)", + [deckId, card_id, level] + ); + } + + res.json({ success: true }); + } catch (err) { + // DB-Trigger Fehler (45000) sauber weitergeben + if (err.sqlState === "45000") { + return res.status(400).json({ error: err.message }); + } + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ════════════════════════════════════════════ + DELETE /api/decks/:id/cards { card_id, level } + Karte aus Deck entfernen (amount - 1, bei 0 löschen) +════════════════════════════════════════════ */ + +router.delete("/decks/:id/cards", async (req, res) => { + const userId = req.session.user.id; + const deckId = req.params.id; + const { card_id, level = 1 } = req.body; + + if (!card_id) return res.status(400).json({ error: "card_id fehlt." }); + + try { + // Deck gehört dem Spieler? + const [[deck]] = await db.query( + "SELECT id FROM decks WHERE id = ? AND user_id = ?", + [deckId, userId] + ); + if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." }); + + const [[entry]] = await db.query( + "SELECT id, amount FROM deck_cards WHERE deck_id = ? AND card_id = ? AND level = ?", + [deckId, card_id, level] + ); + if (!entry) return res.status(404).json({ error: "Karte nicht im Deck." }); + + if (entry.amount > 1) { + await db.query( + "UPDATE deck_cards SET amount = amount - 1 WHERE id = ?", + [entry.id] + ); + } else { + await db.query("DELETE FROM deck_cards WHERE id = ?", [entry.id]); + } + + res.json({ success: true }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +module.exports = router;