diff --git a/public/css/bazaar.css b/public/css/bazaar.css
index 046ef94..46af438 100644
--- a/public/css/bazaar.css
+++ b/public/css/bazaar.css
@@ -476,3 +476,64 @@
align-items: center;
gap: 14px;
}
+
+/* ── Mengenauswahl ───────────────────────── */
+.baz-qty-wrap {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.baz-qty-btn {
+ width: 28px;
+ height: 28px;
+ background: linear-gradient(#3a2810, #1a0f04);
+ border: 2px solid #6b4b2a;
+ border-radius: 6px;
+ color: #f0d9a6;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: .15s;
+ line-height: 1;
+}
+.baz-qty-btn:hover {
+ border-color: #f0d060;
+ color: #fff;
+}
+.baz-qty-input {
+ width: 52px;
+ text-align: center;
+ background: rgba(0,0,0,0.5);
+ border: 2px solid #6b4b2a;
+ border-radius: 6px;
+ color: #f0d9a6;
+ font-family: "Cinzel", serif;
+ font-size: 14px;
+ font-weight: bold;
+ padding: 4px 0;
+ outline: none;
+ -moz-appearance: textfield;
+}
+.baz-qty-input::-webkit-inner-spin-button,
+.baz-qty-input::-webkit-outer-spin-button { -webkit-appearance: none; }
+.baz-qty-input:focus { border-color: #f0d060; }
+.baz-qty-max {
+ padding: 4px 10px;
+ background: linear-gradient(#4a3010, #2a1808);
+ border: 2px solid #c8960c;
+ border-radius: 6px;
+ color: #f0d060;
+ font-family: "Cinzel", serif;
+ font-size: 10px;
+ font-weight: bold;
+ cursor: pointer;
+ letter-spacing: 1px;
+ transition: .15s;
+}
+.baz-qty-max:hover {
+ border-color: #f0d060;
+ color: #fff5cc;
+}
diff --git a/public/js/buildings/bazaar.js b/public/js/buildings/bazaar.js
index a239209..f97ad3e 100644
--- a/public/js/buildings/bazaar.js
+++ b/public/js/buildings/bazaar.js
@@ -179,18 +179,37 @@ async function loadShopCards() {
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 maxAffordable = Math.max(1, Math.min(99,
+ card.price_wood > 0 ? Math.floor(baz_wood / card.price_wood) : 99,
+ card.price_iron > 0 ? Math.floor(baz_iron / card.price_iron) : 99,
+ card.price_gold > 0 ? Math.floor(baz_gold / card.price_gold) : 99,
+ card.price_gems > 0 ? Math.floor(baz_gems / card.price_gems) : 99,
+ ));
- 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(" ");
+ function calcTotal(qty) {
+ return {
+ wood: card.price_wood * qty,
+ iron: card.price_iron * qty,
+ gold: card.price_gold * qty,
+ gems: card.price_gems * qty,
+ };
+ }
+
+ function priceHtml(qty) {
+ const t = calcTotal(qty);
+ return [
+ t.wood > 0 ? `🪵 ${t.wood}` : "",
+ t.iron > 0 ? `⚙️ ${t.iron}` : "",
+ t.gold > 0 ? `🪙 ${t.gold}` : "",
+ t.gems > 0 ? `💎 ${t.gems}` : "",
+ ].filter(Boolean).join(" ");
+ }
+
+ function canAffordQty(qty) {
+ const t = calcTotal(qty);
+ return baz_wood >= t.wood && baz_iron >= t.iron &&
+ baz_gold >= t.gold && baz_gems >= t.gems;
+ }
const modal = document.createElement("div");
modal.id = "baz-confirm-modal";
@@ -202,33 +221,68 @@ function showBuyConfirm(card) {
onerror="this.src='/images/avatar_placeholder.svg'">
${card.name}
- ${priceStr || "Kostenlos"}
- ${!canAfford ? `⚠ Nicht genug Ressourcen
` : ""}
+
+
+
+
+
+
+ ${priceHtml(1) || "Kostenlos"}
+ ⚠ Nicht genug Ressourcen
-
+
`;
document.getElementById("bazaar-popup").appendChild(modal);
+ const qtyInput = modal.querySelector("#baz-qty");
+ const priceEl = modal.querySelector("#baz-total-price");
+ const warnEl = modal.querySelector("#baz-afford-warn");
+ const buyBtn = modal.querySelector("#baz-confirm");
+
+ function updateModal() {
+ const qty = Math.max(1, Math.min(99, parseInt(qtyInput.value) || 1));
+ qtyInput.value = qty;
+ priceEl.innerHTML = priceHtml(qty) || "Kostenlos";
+ const ok = canAffordQty(qty);
+ warnEl.style.display = ok ? "none" : "";
+ buyBtn.disabled = !ok;
+ }
+
+ qtyInput.addEventListener("input", updateModal);
+ modal.querySelector("#baz-qty-minus").onclick = () => {
+ qtyInput.value = Math.max(1, parseInt(qtyInput.value || 1) - 1); updateModal();
+ };
+ modal.querySelector("#baz-qty-plus").onclick = () => {
+ qtyInput.value = Math.min(99, parseInt(qtyInput.value || 1) + 1); updateModal();
+ };
+ modal.querySelector("#baz-qty-max").onclick = () => {
+ qtyInput.value = maxAffordable; updateModal();
+ };
+
+ updateModal();
+
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 = "…";
+
+ buyBtn.onclick = async () => {
+ const qty = Math.max(1, Math.min(99, parseInt(qtyInput.value) || 1));
+ buyBtn.disabled = true; buyBtn.textContent = "…";
try {
const res = await fetch("/api/bazaar/buy", {
method:"POST", headers:{"Content-Type":"application/json"},
- body: JSON.stringify({ card_id: card.id }),
+ body: JSON.stringify({ card_id: card.id, quantity: qty }),
});
const data = await res.json();
- if (!res.ok) { btn.textContent = data.error||"Fehler"; setTimeout(()=>modal.remove(),2000); return; }
+ if (!res.ok) { buyBtn.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!`);
+ showToast(`✅ ${qty}× ${card.name} gekauft!`);
await loadShopCards();
- } catch { btn.textContent="Fehler"; setTimeout(()=>modal.remove(),2000); }
+ } catch { buyBtn.textContent="Fehler"; setTimeout(()=>modal.remove(),2000); }
};
}
diff --git a/routes/bazaar.route.js b/routes/bazaar.route.js
index 3d44d7a..e7cd660 100644
--- a/routes/bazaar.route.js
+++ b/routes/bazaar.route.js
@@ -76,8 +76,9 @@ router.get("/bazaar/cards", requireLogin, async (req, res) => {
════════════════════════════════════════════ */
router.post("/bazaar/buy", requireLogin, async (req, res) => {
const userId = req.session.user.id;
- const { card_id } = req.body;
+ const { card_id, quantity } = req.body;
if (!card_id) return res.status(400).json({ error: "card_id fehlt." });
+ const qty = Math.max(1, Math.min(99, parseInt(quantity) || 1));
try {
const [[card]] = await db.query(
@@ -117,10 +118,10 @@ router.post("/bazaar/buy", requireLogin, async (req, res) => {
{ key: "gems", label: "Gems" },
];
for (const { key, label } of checks) {
- const need = card[`price_${key}`];
+ const need = card[`price_${key}`] * qty;
if (need > 0 && (currency[key] || 0) < need) {
return res.status(400).json({
- error: `Nicht genug ${label}. Benötigt: ${need}, Vorhanden: ${currency[key] || 0}`,
+ error: `Nicht genug ${label}. Benötigt: ${need} (${qty}×), Vorhanden: ${currency[key] || 0}`,
});
}
}
@@ -133,18 +134,18 @@ router.post("/bazaar/buy", requireLogin, async (req, res) => {
gems = gems - ?
WHERE account_id = ?`,
[
- card.price_wood,
- card.price_iron,
- card.price_gold,
- card.price_gems,
+ card.price_wood * qty,
+ card.price_iron * qty,
+ card.price_gold * qty,
+ card.price_gems * qty,
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],
+ `INSERT INTO user_cards (user_id, card_id, amount) VALUES (?, ?, ?)
+ ON DUPLICATE KEY UPDATE amount = amount + ?`,
+ [userId, card_id, qty, qty],
);
const [[updated]] = await db.query(