This commit is contained in:
cay 2026-04-11 14:56:30 +01:00
parent b6ab6fd5ad
commit 350c2e7bdd
4 changed files with 552 additions and 61 deletions

2
app.js
View File

@ -29,6 +29,7 @@ const { registerChatHandlers } = require("./sockets/chat");
const boosterRoutes = require("./routes/booster.route");
const pointsRoutes = require("./routes/points.route");
const combineRoutes = require("./routes/combine.route");
const bazaarRoutes = require("./routes/bazaar.route");
const compression = require("compression");
@ -387,6 +388,7 @@ app.use("/api", boosterRoutes);
app.use("/api", require("./routes/daily.route"));
app.use("/api/points", pointsRoutes);
app.use("/api", combineRoutes);
app.use("/api", bazaarRoutes);
/* ========================
404 Handler

View File

@ -119,3 +119,197 @@
letter-spacing: 2px;
opacity: 0.7;
}
/*
HÄNDLER Karten-Grid
*/
.baz-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 10px;
overflow-y: auto;
padding: 12px 14px;
align-content: start;
}
.baz-card {
position: relative;
border: 2px solid #6b4b2a;
border-radius: 8px;
overflow: visible;
background: #1a0f04;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
aspect-ratio: 3/4;
display: flex;
flex-direction: column;
}
.baz-card:hover {
transform: scale(1.06) translateY(-4px);
border-color: #f0d060;
box-shadow: 0 8px 24px rgba(0,0,0,0.7), 0 0 14px rgba(200,160,60,0.3);
z-index: 10;
}
.baz-card img {
width: 100%; height: 100%;
object-fit: cover; display: block;
border-radius: 6px 6px 0 0;
}
/* Stats */
.baz-stat-atk {
position: absolute; right: 8px; top: 38%;
transform: translateY(-50%);
background: rgba(180,40,20,0.88); border: 1px solid #ff6040;
border-radius: 45px; color: #fff;
font-family: "Cinzel", serif; font-size: 9px; font-weight: bold;
padding: 2px 4px; z-index: 5;
}
.baz-stat-def {
position: absolute; left: 8px; top: 38%;
transform: translateY(-50%);
background: rgba(20,80,180,0.88); border: 1px solid #4090ff;
border-radius: 45px; color: #fff;
font-family: "Cinzel", serif; font-size: 9px; font-weight: bold;
padding: 2px 4px; z-index: 5;
}
.baz-stat-cd {
position: absolute; bottom: 32px; right: 5px;
width: 18px; height: 18px; border-radius: 50%;
background: rgba(0,0,0,0.75); border: 1px solid #f0d060;
display: flex; align-items: center; justify-content: center;
font-family: "Cinzel", serif; font-size: 8px; font-weight: bold;
color: #f0d9a6; z-index: 5; pointer-events: none;
}
/* Rarity */
.baz-rarity {
position: absolute; top: 62%; left: 0; right: 0;
display: flex; justify-content: center; flex-wrap: wrap;
gap: 1px; pointer-events: none; z-index: 4;
}
/* Range / Race */
.baz-range-race {
position: absolute; top: 74%; left: 0; right: 0;
display: flex; justify-content: center; gap: 3px;
pointer-events: none; z-index: 6;
}
.baz-badge-range, .baz-badge-race {
display: flex; align-items: center; gap: 2px;
padding: 1px 3px; border-radius: 20px;
font-family: "Cinzel", serif; font-size: 8px; font-weight: bold; line-height: 1;
}
.baz-badge-range { background: rgba(30,20,0,0.82); border: 1px solid #e8b84b; color: #e8b84b; }
.baz-badge-race { background: rgba(0,25,0,0.82); border: 1px solid #7de87d; color: #7de87d; }
/* Preis-Badge */
.baz-price {
position: absolute; bottom: 0; left: 0; right: 0;
display: flex; justify-content: center; gap: 4px; flex-wrap: wrap;
padding: 2px 3px 3px;
background: linear-gradient(transparent, rgba(0,0,0,0.85));
border-radius: 0 0 6px 6px;
z-index: 7; pointer-events: none;
}
.baz-price-gold, .baz-price-silver {
font-family: "Cinzel", serif; font-size: 8px; font-weight: bold;
line-height: 1.3;
}
.baz-price-wood { color: #c8a050; }
.baz-price-iron { color: #a0b0c0; }
.baz-price-gold { color: #f0d060; }
.baz-price-gems { color: #a060ff; }
.baz-price-silver { color: #c0c0c0; }
/* Pagination */
.baz-pagination {
display: flex; align-items: center; justify-content: center;
gap: 5px; padding: 8px 10px; flex-shrink: 0;
border-top: 1px solid #3a2810;
}
.baz-page-btn {
background: linear-gradient(#3a2810, #1a0f04);
border: 1px solid #8b6a3c; border-radius: 5px;
color: #f0d9a6; font-family: "Cinzel", serif;
font-size: 11px; padding: 3px 10px; cursor: pointer; transition: 0.15s;
}
.baz-page-btn:hover { border-color: #f0d060; }
.baz-page-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.baz-page-btn.baz-page-active {
background: linear-gradient(#6b4b2a, #3c2414);
border-color: #f0d060; color: #fff5cc;
}
.baz-page-info { color: #a08060; font-size: 11px; }
/* Leer / Laden */
.baz-empty, .baz-loading {
grid-column: 1 / -1; text-align: center;
color: #8b6a3c; font-family: "Cinzel", serif;
font-size: 13px; padding: 40px 0;
}
/* Währung im Header */
.baz-currency {
font-family: "Cinzel", serif; font-size: 12px;
color: #f0d9a6; letter-spacing: 1px;
}
/* Kauf-Bestätigung */
.baz-confirm-backdrop {
position: absolute; inset: 0;
background: rgba(0,0,0,0.7);
border-radius: 0 0 10px 10px;
z-index: 50;
}
.baz-confirm-box {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
z-index: 51;
background: linear-gradient(#2a1a08, #1a0f04);
border: 2px solid #c8960c; border-radius: 12px;
padding: 24px 28px; min-width: 260px;
display: flex; flex-direction: column;
align-items: center; gap: 12px;
font-family: "Cinzel", serif;
}
.baz-confirm-img-wrap {
width: 120px; aspect-ratio: 3/4;
border: 2px solid #6b4b2a; border-radius: 8px; overflow: hidden;
}
.baz-confirm-img-wrap img { width: 100%; height: 100%; object-fit: cover; }
.baz-confirm-name { font-size: 15px; font-weight: bold; color: #f0d9a6; }
.baz-confirm-price { font-size: 13px; color: #f0d060; }
.baz-confirm-warn { font-size: 11px; color: #ff6666; }
.baz-confirm-btns { display: flex; gap: 12px; }
.baz-btn-cancel, .baz-btn-buy {
padding: 8px 20px; border-radius: 7px; cursor: pointer;
font-family: "Cinzel", serif; font-size: 12px; font-weight: bold;
transition: 0.2s;
}
.baz-btn-cancel {
background: linear-gradient(#3a2810, #1a0f04);
border: 2px solid #6b4b2a; color: #a08060;
}
.baz-btn-cancel:hover { border-color: #f0d060; color: #f0d9a6; }
.baz-btn-buy {
background: linear-gradient(#6b4b2a, #3c2414);
border: 2px solid #f0d060; color: #fff5cc;
}
.baz-btn-buy:hover:not(:disabled) { box-shadow: 0 0 12px rgba(200,160,60,0.5); }
.baz-btn-buy:disabled { opacity: 0.35; cursor: not-allowed; }
/* Toast */
.baz-toast {
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;
}
/* Header-Währungen */
.baz-header-right {
display: flex; align-items: center; gap: 14px;
}

View File

@ -1,107 +1,252 @@
/* ============================================================
public/js/buildings/bazaar.js
Bazaar eigenes Parchment-Popup (wie Gaststätte)
============================================================ */
const BAZAAR_PER_PAGE = 18;
let baz_initialized = false;
let baz_page = 1;
let baz_wood = 0, baz_iron = 0, baz_gold = 0, baz_gems = 0;
const BAZ_SVG_RANGE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#e8b84b" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2 Q1 8 4 14"/><line x1="4" y1="2" x2="4" y2="14" stroke-width="0.7" stroke-dasharray="2,1.5"/><line x1="4" y1="8" x2="13" y2="8"/><polyline points="11,6 13,8 11,10"/><line x1="5" y1="7" x2="4" y2="8"/><line x1="5" y1="9" x2="4" y2="8"/></svg>`;
const BAZ_SVG_RACE = `<svg viewBox="0 0 16 16" width="10" height="10" style="display:inline;vertical-align:middle;flex-shrink:0" fill="none" stroke="#7de87d" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="2.5" r="1.4" fill="#7de87d" stroke="none"/><line x1="9" y1="4" x2="8" y2="9"/><line x1="8" y1="9" x2="10" y2="14"/><line x1="8" y1="9" x2="6" y2="13"/><line x1="8.5" y1="5.5" x2="11" y2="8"/><line x1="8.5" y1="5.5" x2="6" y2="7"/></svg>`;
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=11) {
const file = RARITY_CRYSTALS[String(rarity)];
if (!file) return "";
const img = `<img src="/images/items/${file}" alt="${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(parseInt(rarity)||0);
}
/* ── CSS einmalig laden ── */
function loadCSS() {
if (!document.querySelector('link[href="/css/bazaar.css"]')) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "/css/bazaar.css";
document.head.appendChild(link);
const l = document.createElement("link");
l.rel = "stylesheet"; l.href = "/css/bazaar.css";
document.head.appendChild(l);
}
}
/* ── Popup-Element erzeugen falls noch nicht vorhanden ── */
function ensurePopup() {
if (document.getElementById("bazaar-popup")) return;
const popup = document.createElement("div");
popup.id = "bazaar-popup";
popup.className = "qm-popup";
popup.innerHTML = `
<div class="qm-popup-header">
<span class="qm-popup-title">Bazaar</span>
<span class="qm-popup-close" id="bazaar-close"></span>
<div class="baz-header-right">
<span class="baz-currency">🪵 <span id="baz-wood"></span></span>
<span class="baz-currency"> <span id="baz-iron"></span></span>
<span class="baz-currency">🪙 <span id="baz-gold"></span></span>
<span class="baz-currency">💎 <span id="baz-gems"></span></span>
<span class="qm-popup-close" id="bazaar-close"></span>
</div>
</div>
<div class="mp-body-wrap">
<aside class="mp-tabs" id="mp-tabs">
<button class="mp-tab mp-tab-active" data-tab="mp-content-1">
<aside class="mp-tabs" id="baz-tabs">
<button class="mp-tab mp-tab-active" data-tab="baz-panel-1">
<span class="mp-tab-dot"></span><span>Händler</span>
</button>
<button class="mp-tab" data-tab="mp-content-2">
<button class="mp-tab" data-tab="baz-panel-2">
<span class="mp-tab-dot"></span><span>Auktionen</span>
</button>
<button class="mp-tab" data-tab="mp-content-3">
<button class="mp-tab" data-tab="baz-panel-3">
<span class="mp-tab-dot"></span><span>Tauschbörse</span>
</button>
</aside>
<div class="mp-content">
<div class="mp-panel active" id="mp-content-1">
<div class="mp-panel active" id="baz-panel-1">
<div class="mp-col-header">Händler</div>
<div class="mp-panel-body">
<span class="mp-coming-soon">Demnächst verfügbar</span>
</div>
<div class="baz-grid" id="baz-grid"><div class="baz-loading">Lade Karten</div></div>
<div class="baz-pagination" id="baz-pagination"></div>
</div>
<div class="mp-panel" id="mp-content-2">
<div class="mp-panel" id="baz-panel-2">
<div class="mp-col-header">Auktionen</div>
<div class="mp-panel-body">
<span class="mp-coming-soon">Demnächst verfügbar</span>
</div>
<div class="mp-panel-body"><span class="mp-coming-soon">Demnächst verfügbar</span></div>
</div>
<div class="mp-panel" id="mp-content-3">
<div class="mp-panel" id="baz-panel-3">
<div class="mp-col-header">Tauschbörse</div>
<div class="mp-panel-body">
<span class="mp-coming-soon">Demnächst verfügbar</span>
</div>
<div class="mp-panel-body"><span class="mp-coming-soon">Demnächst verfügbar</span></div>
</div>
</div>
</div>
`;
</div>`;
document.body.appendChild(popup);
/* Schließen-Button */
popup.querySelector("#bazaar-close").addEventListener("click", closeBazaar);
/* Tab-Klick */
popup.querySelectorAll(".mp-tab").forEach((btn) => {
btn.addEventListener("click", () => {
popup.querySelectorAll(".mp-tab").forEach((t) => t.classList.remove("mp-tab-active"));
popup.querySelectorAll(".mp-panel").forEach((p) => p.classList.remove("active"));
popup.querySelectorAll(".mp-tab").forEach(t => t.classList.remove("mp-tab-active"));
popup.querySelectorAll(".mp-panel").forEach(p => p.classList.remove("active"));
btn.classList.add("mp-tab-active");
document.getElementById(btn.dataset.tab)?.classList.add("active");
});
});
/* Drag-fähig machen */
/* Drag */
const header = popup.querySelector(".qm-popup-header");
let isDragging = false, startX, startY, startLeft, startTop;
header.style.cursor = "grab";
header.addEventListener("mousedown", (e) => {
if (e.target.classList.contains("qm-popup-close")) return;
isDragging = true;
header.style.cursor = "grabbing";
const rect = popup.getBoundingClientRect();
startX = e.clientX; startY = e.clientY;
startLeft = rect.left; startTop = rect.top;
popup.style.transform = "none";
popup.style.left = startLeft + "px";
popup.style.top = startTop + "px";
let dragging=false, sx,sy,sl,st;
header.style.cursor="grab";
header.addEventListener("mousedown",(e)=>{
if(e.target.classList.contains("qm-popup-close"))return;
dragging=true; header.style.cursor="grabbing";
const r=popup.getBoundingClientRect();
sx=e.clientX;sy=e.clientY;sl=r.left;st=r.top;
popup.style.transform="none";
popup.style.left=sl+"px";popup.style.top=st+"px";
e.preventDefault();
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
popup.style.left = (startLeft + (e.clientX - startX)) + "px";
popup.style.top = (startTop + (e.clientY - startY)) + "px";
});
document.addEventListener("mouseup", () => {
if (!isDragging) return;
isDragging = false;
header.style.cursor = "grab";
document.addEventListener("mousemove",(e)=>{
if(!dragging)return;
popup.style.left=(sl+(e.clientX-sx))+"px";
popup.style.top=(st+(e.clientY-sy))+"px";
});
document.addEventListener("mouseup",()=>{dragging=false;header.style.cursor="grab";});
}
function updateCurrencyDisplay() {
const fmt = n => n.toLocaleString("de-DE");
const w=document.getElementById("baz-wood"); if(w) w.textContent=fmt(baz_wood);
const i=document.getElementById("baz-iron"); if(i) i.textContent=fmt(baz_iron);
const g=document.getElementById("baz-gold"); if(g) g.textContent=fmt(baz_gold);
const d=document.getElementById("baz-gems"); if(d) d.textContent=fmt(baz_gems);
}
async function loadShopCards() {
const grid = document.getElementById("baz-grid");
const pagination = document.getElementById("baz-pagination");
if (!grid) return;
grid.innerHTML = `<div class="baz-loading">Lade Karten…</div>`;
if (pagination) pagination.innerHTML = "";
try {
const res = await fetch(`/api/bazaar/cards?page=${baz_page}&limit=${BAZAAR_PER_PAGE}`);
if (!res.ok) throw new Error(res.status);
const data = await res.json();
baz_wood=data.wood; baz_iron=data.iron; baz_gold=data.gold; baz_gems=data.gems;
updateCurrencyDisplay();
if (!data.cards.length) {
grid.innerHTML = `<div class="baz-empty">Keine Karten verfügbar.</div>`;
return;
}
grid.innerHTML = data.cards.map((c) => {
const prices = [
c.price_wood > 0 ? `<span class="baz-price-wood">🪵 ${c.price_wood}</span>` : "",
c.price_iron > 0 ? `<span class="baz-price-iron">⚙️ ${c.price_iron}</span>` : "",
c.price_gold > 0 ? `<span class="baz-price-gold">🪙 ${c.price_gold}</span>` : "",
c.price_gems > 0 ? `<span class="baz-price-gems">💎 ${c.price_gems}</span>` : "",
].filter(Boolean).join("");
return `
<div class="baz-card" data-card-id="${c.id}" title="${c.name}">
<img src="/images/cards/${c.image}" alt="${c.name}"
onerror="this.src='/images/avatar_placeholder.svg'">
${c.attack != null ? `<span class="baz-stat-atk">${c.attack}</span>` : ""}
${c.defends != null ? `<span class="baz-stat-def">${c.defends}</span>` : ""}
${c.cooldown != null ? `<span class="baz-stat-cd">${c.cooldown}</span>` : ""}
${c.rarity ? `<div class="baz-rarity">${rarityImgs(c.rarity,11)}</div>` : ""}
${(c.range != null || c.race != null) ? `
<div class="baz-range-race">
${c.range != null ? `<span class="baz-badge-range">${BAZ_SVG_RANGE}&thinsp;${c.range}</span>` : ""}
${c.race != null ? `<span class="baz-badge-race">${BAZ_SVG_RACE}&thinsp;${c.race}</span>` : ""}
</div>` : ""}
<div class="baz-price">${prices || "<span style='color:#8b6a3c;font-size:8px;'>Kostenlos</span>"}</div>
</div>`;
}).join("");
grid.querySelectorAll(".baz-card").forEach((el) => {
el.addEventListener("click", () => {
const card = data.cards.find(c => c.id === parseInt(el.dataset.cardId));
if (card) showBuyConfirm(card);
});
});
renderPagination(pagination, data.totalPages, data.total);
} catch (err) {
grid.innerHTML = `<div class="baz-empty">Fehler beim Laden.</div>`;
console.error("[bazaar]", err);
}
}
function showBuyConfirm(card) {
document.getElementById("baz-confirm-modal")?.remove();
const canAfford =
baz_wood >= card.price_wood &&
baz_iron >= card.price_iron &&
baz_gold >= card.price_gold &&
baz_gems >= card.price_gems;
const priceStr = [
card.price_wood > 0 ? `🪵 ${card.price_wood} Holz` : "",
card.price_iron > 0 ? `⚙️ ${card.price_iron} Eisen` : "",
card.price_gold > 0 ? `🪙 ${card.price_gold} Gold` : "",
card.price_gems > 0 ? `💎 ${card.price_gems} Gems` : "",
].filter(Boolean).join(" ");
const modal = document.createElement("div");
modal.id = "baz-confirm-modal";
modal.innerHTML = `
<div class="baz-confirm-backdrop"></div>
<div class="baz-confirm-box">
<div class="baz-confirm-img-wrap">
<img src="/images/cards/${card.image}" alt="${card.name}"
onerror="this.src='/images/avatar_placeholder.svg'">
</div>
<div class="baz-confirm-name">${card.name}</div>
<div class="baz-confirm-price">${priceStr || "Kostenlos"}</div>
${!canAfford ? `<div class="baz-confirm-warn">⚠ Nicht genug Ressourcen</div>` : ""}
<div class="baz-confirm-btns">
<button class="baz-btn-cancel" id="baz-cancel">Abbrechen</button>
<button class="baz-btn-buy" id="baz-confirm" ${!canAfford?"disabled":""}>Kaufen</button>
</div>
</div>`;
document.getElementById("bazaar-popup").appendChild(modal);
modal.querySelector("#baz-cancel").onclick = () => modal.remove();
modal.querySelector(".baz-confirm-backdrop").onclick = () => modal.remove();
modal.querySelector("#baz-confirm").onclick = async () => {
const btn = modal.querySelector("#baz-confirm");
btn.disabled = true; btn.textContent = "…";
try {
const res = await fetch("/api/bazaar/buy", {
method:"POST", headers:{"Content-Type":"application/json"},
body: JSON.stringify({ card_id: card.id }),
});
const data = await res.json();
if (!res.ok) { btn.textContent = data.error||"Fehler"; setTimeout(()=>modal.remove(),2000); return; }
baz_wood=data.wood; baz_iron=data.iron; baz_gold=data.gold; baz_gems=data.gems;
updateCurrencyDisplay();
modal.remove();
showToast(`${card.name} gekauft!`);
await loadShopCards();
} catch { btn.textContent="Fehler"; setTimeout(()=>modal.remove(),2000); }
};
}
function renderPagination(el, totalPages, total) {
if (!el||!totalPages||totalPages<=1) return;
el.innerHTML = `
<button class="baz-page-btn" id="baz-prev" ${baz_page===1?"disabled":""}></button>
${Array.from({length:totalPages},(_,i)=>i+1).map(p=>
`<button class="baz-page-btn ${p===baz_page?"baz-page-active":""}" data-page="${p}">${p}</button>`
).join("")}
<button class="baz-page-btn" id="baz-next" ${baz_page===totalPages?"disabled":""}></button>
<span class="baz-page-info">${total} Karten</span>`;
el.querySelector("#baz-prev")?.addEventListener("click",async()=>{if(baz_page>1){baz_page--;await loadShopCards();}});
el.querySelector("#baz-next")?.addEventListener("click",async()=>{if(baz_page<totalPages){baz_page++;await loadShopCards();}});
el.querySelectorAll("[data-page]").forEach(btn=>btn.addEventListener("click",async()=>{baz_page=parseInt(btn.dataset.page);await loadShopCards();}));
}
function showToast(msg) {
const t=document.createElement("div"); t.className="baz-toast"; t.textContent=msg;
document.body.appendChild(t); setTimeout(()=>t.remove(),2800);
}
function closeBazaar() {
@ -109,17 +254,14 @@ function closeBazaar() {
document.getElementById("qm-overlay")?.classList.remove("active");
}
/* ── Öffentliche Funktion ── */
export function loadBazaar() {
loadCSS();
ensurePopup();
const popup = document.getElementById("bazaar-popup");
const overlay = document.getElementById("qm-overlay");
popup.style.left = "50%";
popup.style.top = "50%";
popup.style.transform = "translate(-50%, -50%) scale(1)";
const popup=document.getElementById("bazaar-popup");
const overlay=document.getElementById("qm-overlay");
popup.style.left="50%"; popup.style.top="50%";
popup.style.transform="translate(-50%, -50%) scale(1)";
popup.classList.add("active");
overlay?.classList.add("active");
if (!baz_initialized) { baz_initialized=true; baz_page=1; loadShopCards(); }
}

153
routes/bazaar.route.js Normal file
View File

@ -0,0 +1,153 @@
/* ============================================================
routes/bazaar.route.js
============================================================ */
const express = require("express");
const router = express.Router();
const db = require("../database/database");
function requireLogin(req, res, next) {
if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" });
next();
}
function getMaxRarity(playerLevel) {
if (playerLevel < 10) return 2;
if (playerLevel < 20) return 3;
if (playerLevel < 30) return 4;
if (playerLevel < 40) return 5;
return 6;
}
/*
GET /api/bazaar/cards?page=1&limit=18
*/
router.get("/bazaar/cards", requireLogin, async (req, res) => {
const userId = req.session.user.id;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 18;
const offset = (page - 1) * limit;
try {
const [[account]] = await db.query("SELECT level FROM accounts WHERE id = ?", [userId]);
const maxRarity = getMaxRarity(account?.level ?? 1);
const [cards] = await db.query(
`SELECT
c.id, c.name, c.image, c.rarity,
c.attack, c.defends, c.cooldown, c.\`range\`, c.\`race\`,
COALESCE(p.price_wood, 0) AS price_wood,
COALESCE(p.price_iron, 0) AS price_iron,
COALESCE(p.price_gold, 0) AS price_gold,
COALESCE(p.price_gems, 0) AS price_gems
FROM cards c
LEFT JOIN card_shop_prices p ON p.rarity = c.rarity
WHERE c.rarity <= ?
ORDER BY c.rarity ASC, c.name ASC
LIMIT ? OFFSET ?`,
[maxRarity, limit, offset]
);
const [[{ total }]] = await db.query(
"SELECT COUNT(*) AS total FROM cards WHERE rarity <= ?", [maxRarity]
);
const [[currency]] = await db.query(
"SELECT wood, iron, gold, gems FROM account_currency WHERE account_id = ?", [userId]
);
res.json({
cards, total, page,
totalPages: Math.ceil(total / limit),
wood: currency?.wood || 0,
iron: currency?.iron || 0,
gold: currency?.gold || 0,
gems: currency?.gems || 0,
});
} catch (err) {
console.error("[bazaar/cards]", err);
res.status(500).json({ error: "DB Fehler" });
}
});
/*
POST /api/bazaar/buy { card_id }
*/
router.post("/bazaar/buy", requireLogin, async (req, res) => {
const userId = req.session.user.id;
const { card_id } = req.body;
if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
try {
const [[card]] = await db.query(
`SELECT c.id, c.name, c.rarity,
COALESCE(p.price_wood, 0) AS price_wood,
COALESCE(p.price_iron, 0) AS price_iron,
COALESCE(p.price_gold, 0) AS price_gold,
COALESCE(p.price_gems, 0) AS price_gems
FROM cards c
LEFT JOIN card_shop_prices p ON p.rarity = c.rarity
WHERE c.id = ?`, [card_id]
);
if (!card) return res.status(404).json({ error: "Karte nicht gefunden." });
const [[account]] = await db.query("SELECT level FROM accounts WHERE id = ?", [userId]);
if (parseInt(card.rarity) > getMaxRarity(account?.level ?? 1)) {
return res.status(403).json({ error: "Dein Level reicht für diese Karte nicht aus." });
}
const [[currency]] = await db.query(
"SELECT wood, iron, gold, gems FROM account_currency WHERE account_id = ?", [userId]
);
if (!currency) return res.status(400).json({ error: "Keine Währungsdaten gefunden." });
const checks = [
{ key: "wood", label: "Holz" },
{ key: "iron", label: "Eisen" },
{ key: "gold", label: "Gold" },
{ key: "gems", label: "Gems" },
];
for (const { key, label } of checks) {
const need = card[`price_${key}`];
if (need > 0 && (currency[key] || 0) < need) {
return res.status(400).json({
error: `Nicht genug ${label}. Benötigt: ${need}, Vorhanden: ${currency[key] || 0}`
});
}
}
await db.query(
`UPDATE account_currency
SET wood = wood - ?,
iron = iron - ?,
gold = gold - ?,
gems = gems - ?
WHERE account_id = ?`,
[card.price_wood, card.price_iron, card.price_gold, card.price_gems, userId]
);
await db.query(
`INSERT INTO user_cards (user_id, card_id, amount) VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE amount = amount + 1`,
[userId, card_id]
);
const [[updated]] = await db.query(
"SELECT wood, iron, gold, gems FROM account_currency WHERE account_id = ?", [userId]
);
res.json({
success: true,
card: { id: card.id, name: card.name },
wood: updated?.wood || 0,
iron: updated?.iron || 0,
gold: updated?.gold || 0,
gems: updated?.gems || 0,
});
} catch (err) {
console.error("[bazaar/buy]", err);
res.status(500).json({ error: "DB Fehler" });
}
});
module.exports = router;