From b486199ca251b1665aa2c3dc5d426f05a5b52988 Mon Sep 17 00:00:00 2001 From: cay Date: Tue, 14 Apr 2026 18:25:19 +0100 Subject: [PATCH] q3z34 --- public/css/bazaar.css | 61 ++++++++++++++++++++++ public/js/buildings/bazaar.js | 96 +++++++++++++++++++++++++++-------- routes/bazaar.route.js | 21 ++++---- 3 files changed, 147 insertions(+), 31 deletions(-) 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"}
+
- +
`; 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(