sjrt
This commit is contained in:
parent
b6ab6fd5ad
commit
350c2e7bdd
2
app.js
2
app.js
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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} ${c.range}</span>` : ""}
|
||||
${c.race != null ? `<span class="baz-badge-race">${BAZ_SVG_RACE} ${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
153
routes/bazaar.route.js
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user