This commit is contained in:
cay 2026-04-04 10:28:05 +01:00
parent 7b999906fb
commit c6234d953d
2 changed files with 62 additions and 57 deletions

View File

@ -3,8 +3,8 @@ const CARDS_PER_PAGE = 18;
let currentGroupId = null; let currentGroupId = null;
let currentPage = 1; let currentPage = 1;
let currentDeckId = null; let currentDeckId = null;
let deckCards = []; // [{card_id, level, amount}] let deckCards = []; // [{card_id, amount}]
let userCardsCache = []; // aktuelle Seite: [{card_id, level, amount, name, image, attack, defense}] let userCardsCache = []; // aktuelle Seite: [{card_id, amount, name, image, rarity, attack, defense}]
let decks = []; // [{id, name}] let decks = []; // [{id, name}]
@ -19,10 +19,12 @@ const RARITY_CRYSTALS = {
"7": "pinker-cristal.png", "7": "pinker-cristal.png",
}; };
function rarityImg(rarity, size = 16) { function rarityImgs(rarity, size = 14) {
const file = RARITY_CRYSTALS[String(rarity)]; const file = RARITY_CRYSTALS[String(rarity)];
if (!file) return ""; if (!file) return "";
return `<img src="/images/items/${file}" alt="Stufe ${rarity}" style="width:${size}px;height:${size}px;object-fit:contain;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.8));">`; const count = parseInt(rarity) || 0;
const img = `<img src="/images/items/${file}" alt="Stufe ${rarity}" style="width:${size}px;height:${size}px;object-fit:contain;filter:drop-shadow(0 1px 2px rgba(0,0,0,0.8));">`;
return img.repeat(count);
} }
/* /*
@ -415,14 +417,19 @@ function renderShell() {
} }
.kd-deck-count-indeck { font-family: "Cinzel", serif; font-size: 13px; color: #88cc44; font-weight: bold; } .kd-deck-count-indeck { font-family: "Cinzel", serif; font-size: 13px; color: #88cc44; font-weight: bold; }
/* ── Rarity Kristall ───────────────────── */ /* ── Rarity Kristalle ───────────────────── */
.kd-rarity { .kd-rarity {
position: absolute; display: flex;
top: 3px; justify-content: center;
right: 4px; align-items: center;
z-index: 5; flex-wrap: wrap;
pointer-events: none; gap: 1px;
line-height: 0; padding: 3px 4px;
background: rgba(0,0,0,0.82);
border-top: 1px solid #3a2810;
flex-shrink: 0;
min-height: 20px;
border-radius: 0 0 6px 6px;
} }
/* ── Empty / Loading States ──────────────── */ /* ── Empty / Loading States ──────────────── */
@ -612,18 +619,18 @@ async function addCardToDeck(card) {
return; return;
} }
// Level > 5: max. 1 Exemplar im Deck // Rarity > 5: max. 1 Exemplar im Deck
if (card.level > 5) { if (parseInt(card.rarity) > 5) {
const already = deckCards.find(c => c.card_id === card.card_id && c.level === card.level); const already = deckCards.find(c => c.card_id === card.card_id);
if (already) { if (already) {
showToast("Karten ab Level 6 dürfen nur 1x im Deck sein."); showToast("Karten ab Rarity 6 dürfen nur 1x im Deck sein.");
return; return;
} }
} }
// Nicht mehr als vorhanden besitzt // Nicht mehr als vorhanden besitzt
const owned = card.amount; const owned = card.amount;
const inDeck = getDeckCount(card.card_id, card.level); const inDeck = getDeckCount(card.card_id);
if (inDeck >= owned) { if (inDeck >= owned) {
showToast("Du hast keine weiteren Exemplare dieser Karte."); showToast("Du hast keine weiteren Exemplare dieser Karte.");
return; return;
@ -633,7 +640,7 @@ async function addCardToDeck(card) {
const res = await fetch(`/api/decks/${currentDeckId}/cards`, { const res = await fetch(`/api/decks/${currentDeckId}/cards`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ card_id: card.card_id, level: card.level }), body: JSON.stringify({ card_id: card.card_id }),
}); });
if (!res.ok) { if (!res.ok) {
const err = await res.json(); const err = await res.json();
@ -655,7 +662,7 @@ async function removeCardFromDeck(card) {
const res = await fetch(`/api/decks/${currentDeckId}/cards`, { const res = await fetch(`/api/decks/${currentDeckId}/cards`, {
method: "DELETE", method: "DELETE",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ card_id: card.card_id, level: card.level }), body: JSON.stringify({ card_id: card.card_id }),
}); });
if (!res.ok) { if (!res.ok) {
const err = await res.json(); const err = await res.json();
@ -712,14 +719,13 @@ function renderCollectionGrid(grid, cards) {
const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0); const totalInDeck = deckCards.reduce((s, c) => s + c.amount, 0);
grid.innerHTML = cards.map(c => { grid.innerHTML = cards.map(c => {
const inDeck = getDeckCount(c.card_id, c.level); const inDeck = getDeckCount(c.card_id);
const maxed = isMaxedOut(c, inDeck, totalInDeck); const maxed = isMaxedOut(c, inDeck, totalInDeck);
return ` return `
<div class="kd-card ${maxed ? "kd-card-maxed" : ""}" <div class="kd-card ${maxed ? "kd-card-maxed" : ""}"
data-card-id="${c.card_id}" data-level="${c.level}" data-card-id="${c.card_id}"
title="${c.name} (Level ${c.level})"> title="${c.name}">
<div class="kd-card-name">${c.name}</div> <div class="kd-card-name">${c.name}</div>
${c.rarity ? `<span class="kd-rarity">${rarityImg(c.rarity, 15)}</span>` : ""}
<img src="/images/cards/${c.image}" alt="${c.name}" <img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'"> onerror="this.src='/images/avatar_placeholder.svg'">
${c.attack != null ? `<span class="kd-stat-atk">${c.attack}</span>` : ""} ${c.attack != null ? `<span class="kd-stat-atk">${c.attack}</span>` : ""}
@ -728,14 +734,13 @@ function renderCollectionGrid(grid, cards) {
<span class="kd-count-owned" title="Besitzt du">${c.amount}×</span> <span class="kd-count-owned" title="Besitzt du">${c.amount}×</span>
<span class="kd-count-deck" title="Im Deck">🃏 ${inDeck}</span> <span class="kd-count-deck" title="Im Deck">🃏 ${inDeck}</span>
</div> </div>
${c.rarity ? `<div class="kd-rarity">${rarityImgs(c.rarity, 13)}</div>` : ""}
</div>`; </div>`;
}).join(""); }).join("");
grid.querySelectorAll(".kd-card:not(.kd-card-maxed)").forEach(el => { grid.querySelectorAll(".kd-card:not(.kd-card-maxed)").forEach(el => {
el.addEventListener("click", async () => { el.addEventListener("click", async () => {
const card = cards.find( const card = cards.find(c => c.card_id === parseInt(el.dataset.cardId));
c => c.card_id === parseInt(el.dataset.cardId) && c.level === parseInt(el.dataset.level)
);
if (card) await addCardToDeck(card); if (card) await addCardToDeck(card);
}); });
}); });
@ -751,11 +756,10 @@ function renderDeckGrid(grid, cards) {
} }
// Owned-Anzahl aus userCardsCache auslesen // Owned-Anzahl aus userCardsCache auslesen
grid.innerHTML = cards.map(c => { grid.innerHTML = cards.map(c => {
const ownedEntry = userCardsCache.find(u => u.card_id === c.card_id && u.level === c.level); const ownedEntry = userCardsCache.find(u => u.card_id === c.card_id);
const ownedAmt = ownedEntry ? ownedEntry.amount : "?"; const ownedAmt = ownedEntry ? ownedEntry.amount : "?";
return ` return `
<div class="kd-deck-card" data-card-id="${c.card_id}" data-level="${c.level}" title="Klicken zum Entfernen: ${c.name}"> <div class="kd-deck-card" data-card-id="${c.card_id}" title="Klicken zum Entfernen: ${c.name}">
${c.rarity ? `<span class="kd-rarity">${rarityImg(c.rarity, 18)}</span>` : ""}
<img src="/images/cards/${c.image}" alt="${c.name}" <img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'"> onerror="this.src='/images/avatar_placeholder.svg'">
<div class="kd-deck-card-footer"> <div class="kd-deck-card-footer">
@ -764,14 +768,13 @@ function renderDeckGrid(grid, cards) {
<div class="kd-deck-card-footer-counts"> <div class="kd-deck-card-footer-counts">
<span class="kd-deck-count-indeck" title="Im Deck">🃏 ${c.amount}</span> <span class="kd-deck-count-indeck" title="Im Deck">🃏 ${c.amount}</span>
</div> </div>
${c.rarity ? `<div class="kd-rarity">${rarityImgs(c.rarity, 14)}</div>` : ""}
</div>`; </div>`;
}).join(""); }).join("");
grid.querySelectorAll(".kd-deck-card").forEach(el => { grid.querySelectorAll(".kd-deck-card").forEach(el => {
el.addEventListener("click", async () => { el.addEventListener("click", async () => {
const card = cards.find( const card = cards.find(c => c.card_id === parseInt(el.dataset.cardId));
c => c.card_id === parseInt(el.dataset.cardId) && c.level === parseInt(el.dataset.level)
);
if (card) await removeCardFromDeck(card); if (card) await removeCardFromDeck(card);
}); });
}); });
@ -878,8 +881,8 @@ function showNewDeckModal() {
/* /*
HELPERS HELPERS
*/ */
function getDeckCount(cardId, level) { function getDeckCount(cardId) {
const entry = deckCards.find(c => c.card_id === cardId && c.level === level); const entry = deckCards.find(c => c.card_id === cardId);
return entry ? entry.amount : 0; return entry ? entry.amount : 0;
} }
@ -887,7 +890,7 @@ function isMaxedOut(card, inDeck, totalInDeck) {
if (!currentDeckId) return false; if (!currentDeckId) return false;
if (totalInDeck >= 30) return true; if (totalInDeck >= 30) return true;
if (inDeck >= card.amount) return true; // mehr als besessen if (inDeck >= card.amount) return true; // mehr als besessen
if (card.level > 5 && inDeck >= 1) return true; // Level > 5: nur 1x if (parseInt(card.rarity) > 5 && inDeck >= 1) return true; // Rarity > 5: nur 1x
return false; return false;
} }

View File

@ -73,7 +73,6 @@ router.get("/user-cards", async (req, res) => {
const [cards] = await db.query( const [cards] = await db.query(
`SELECT `SELECT
uc.card_id, uc.card_id,
uc.level,
uc.amount, uc.amount,
c.name, c.name,
c.image, c.image,
@ -86,9 +85,9 @@ router.get("/user-cards", async (req, res) => {
FROM user_cards uc FROM user_cards uc
JOIN cards c ON c.id = uc.card_id JOIN cards c ON c.id = uc.card_id
JOIN card_groups cg ON cg.id = c.group_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 LEFT JOIN card_levels cl ON cl.card_id = uc.card_id AND cl.level = 1
WHERE uc.user_id = ? AND c.group_id = ? WHERE uc.user_id = ? AND c.group_id = ?
ORDER BY c.id, uc.level ORDER BY c.id
LIMIT ? OFFSET ?`, LIMIT ? OFFSET ?`,
[userId, group_id, parseInt(limit), parseInt(offset)] [userId, group_id, parseInt(limit), parseInt(offset)]
); );
@ -186,7 +185,6 @@ router.get("/decks/:id/cards", async (req, res) => {
const [cards] = await db.query( const [cards] = await db.query(
`SELECT `SELECT
dc.card_id, dc.card_id,
dc.level,
dc.amount, dc.amount,
c.name, c.name,
c.image, c.image,
@ -195,9 +193,9 @@ router.get("/decks/:id/cards", async (req, res) => {
cl.defense cl.defense
FROM deck_cards dc FROM deck_cards dc
JOIN cards c ON c.id = dc.card_id 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 LEFT JOIN card_levels cl ON cl.card_id = dc.card_id AND cl.level = 1
WHERE dc.deck_id = ? WHERE dc.deck_id = ?
ORDER BY c.name, dc.level`, ORDER BY c.name`,
[deckId] [deckId]
); );
res.json(cards); res.json(cards);
@ -215,7 +213,7 @@ router.get("/decks/:id/cards", async (req, res) => {
router.post("/decks/:id/cards", async (req, res) => { router.post("/decks/:id/cards", async (req, res) => {
const userId = req.session.user.id; const userId = req.session.user.id;
const deckId = req.params.id; const deckId = req.params.id;
const { card_id, level = 1 } = req.body; const { card_id } = req.body;
if (!card_id) return res.status(400).json({ error: "card_id fehlt." }); if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
@ -227,10 +225,10 @@ router.post("/decks/:id/cards", async (req, res) => {
); );
if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." }); if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." });
// Besitzt der Spieler diese Karte mit diesem Level? // Besitzt der Spieler diese Karte?
const [[owned]] = await db.query( const [[owned]] = await db.query(
"SELECT amount FROM user_cards WHERE user_id = ? AND card_id = ? AND level = ?", "SELECT amount FROM user_cards WHERE user_id = ? AND card_id = ?",
[userId, card_id, level] [userId, card_id]
); );
if (!owned) return res.status(400).json({ error: "Du besitzt diese Karte nicht." }); if (!owned) return res.status(400).json({ error: "Du besitzt diese Karte nicht." });
@ -243,14 +241,18 @@ router.post("/decks/:id/cards", async (req, res) => {
return res.status(400).json({ error: "Deck ist voll (max. 30 Karten)." }); return res.status(400).json({ error: "Deck ist voll (max. 30 Karten)." });
} }
// Level > 5: max. 1× im Deck // Rarity > 5: max. 1× im Deck
const [[cardInfo]] = await db.query(
"SELECT rarity FROM cards WHERE id = ?",
[card_id]
);
const [[existing]] = await db.query( const [[existing]] = await db.query(
"SELECT amount FROM deck_cards WHERE deck_id = ? AND card_id = ? AND level = ?", "SELECT amount FROM deck_cards WHERE deck_id = ? AND card_id = ?",
[deckId, card_id, level] [deckId, card_id]
); );
if (level > 5 && existing) { if (parseInt(cardInfo?.rarity) > 5 && existing) {
return res.status(400).json({ error: "Karten ab Level 6 dürfen nur einmal im Deck sein." }); return res.status(400).json({ error: "Karten ab Rarity 6 dürfen nur einmal im Deck sein." });
} }
// Nicht mehr einfügen als besessen // Nicht mehr einfügen als besessen
@ -262,13 +264,13 @@ router.post("/decks/:id/cards", async (req, res) => {
// Einfügen oder erhöhen // Einfügen oder erhöhen
if (existing) { if (existing) {
await db.query( await db.query(
"UPDATE deck_cards SET amount = amount + 1 WHERE deck_id = ? AND card_id = ? AND level = ?", "UPDATE deck_cards SET amount = amount + 1 WHERE deck_id = ? AND card_id = ?",
[deckId, card_id, level] [deckId, card_id]
); );
} else { } else {
await db.query( await db.query(
"INSERT INTO deck_cards (deck_id, card_id, level, amount) VALUES (?, ?, ?, 1)", "INSERT INTO deck_cards (deck_id, card_id, amount) VALUES (?, ?, 1)",
[deckId, card_id, level] [deckId, card_id]
); );
} }
@ -291,7 +293,7 @@ router.post("/decks/:id/cards", async (req, res) => {
router.delete("/decks/:id/cards", async (req, res) => { router.delete("/decks/:id/cards", async (req, res) => {
const userId = req.session.user.id; const userId = req.session.user.id;
const deckId = req.params.id; const deckId = req.params.id;
const { card_id, level = 1 } = req.body; const { card_id } = req.body;
if (!card_id) return res.status(400).json({ error: "card_id fehlt." }); if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
@ -304,8 +306,8 @@ router.delete("/decks/:id/cards", async (req, res) => {
if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." }); if (!deck) return res.status(404).json({ error: "Deck nicht gefunden." });
const [[entry]] = await db.query( const [[entry]] = await db.query(
"SELECT id, amount FROM deck_cards WHERE deck_id = ? AND card_id = ? AND level = ?", "SELECT id, amount FROM deck_cards WHERE deck_id = ? AND card_id = ?",
[deckId, card_id, level] [deckId, card_id]
); );
if (!entry) return res.status(404).json({ error: "Karte nicht im Deck." }); if (!entry) return res.status(404).json({ error: "Karte nicht im Deck." });