diff --git a/.env b/.env index e541584..2652e8e 100644 --- a/.env +++ b/.env @@ -15,4 +15,7 @@ SESSION_SECRET=irgendein_langer_geheimer_zufallstext_123! MAIL_HOST=smtp.ionos.de MAIL_USER=register@dynastyofknights.com -MAIL_PASS=111168-j-62217DwmbwPK \ No newline at end of file +MAIL_PASS=111168-j-62217DwmbwPK + +STRIPE_SECRET_KEY=sk_live_:J1d5&Sa0!_~>lLHuyxa$VC:AuOh +STRIPE_WEBHOOK_SECRET=whsec_oaZPQsV2Hj { [userId], ); const [[currency]] = await db.query( - "SELECT silver, gold, gems, wood, stone FROM account_currency WHERE account_id = ?", + "SELECT silver, gold, gems, wood, stone, iron FROM account_currency WHERE account_id = ?", [userId], ); res.json({ @@ -329,6 +338,7 @@ app.get("/api/hud", requireLogin, async (req, res) => { gems: currency?.gems || 0, wood: currency?.wood || 0, stone: currency?.stone || 0, + iron: currency?.iron || 0, }); } catch (err) { console.error(err); @@ -385,6 +395,7 @@ app.use("/arena", arenaRoutes); app.use("/api", boosterRoutes); app.use("/api", require("./routes/daily.route")); app.use("/api/points", pointsRoutes); +app.use("/api", shopRoutes); /* ======================== 404 Handler diff --git a/public/css/hud.css b/public/css/hud.css index 1bd5257..2b4ff49 100644 --- a/public/css/hud.css +++ b/public/css/hud.css @@ -300,3 +300,202 @@ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); display: inline-block; } + +/* ========================= + Shop Overlay & Popup +========================= */ + +#shop-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + z-index: 3999; + display: none; +} +#shop-overlay.active { display: block; } + +#shop-popup { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0.92); + opacity: 0; + pointer-events: none; + + width: min(1100px, 95vw); + max-height: 90vh; + overflow-y: auto; + + background: url("/images/parchment.png") center / cover no-repeat; + border: 4px solid #6b4b2a; + border-radius: 12px; + box-shadow: 0 0 60px rgba(0,0,0,0.95); + + z-index: 4000; + display: flex; + flex-direction: column; + + transition: opacity 0.25s ease, transform 0.25s ease; +} +#shop-popup.active { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + pointer-events: all; +} + +#shop-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + background: linear-gradient(#6b4b2a, #3c2414); + border-bottom: 2px solid #8b6a3c; + border-radius: 8px 8px 0 0; + flex-shrink: 0; +} + +#shop-title { + font-family: "Tangerine", serif; + font-size: 46px; + color: #f0d9a6; + text-shadow: 0 2px 6px black; +} + +#shop-close { + font-size: 22px; + color: #f0d9a6; + cursor: pointer; + width: 34px; height: 34px; + display: flex; align-items: center; justify-content: center; + border-radius: 6px; + transition: 0.2s; +} +#shop-close:hover { background: rgba(255,255,255,0.1); color: #fff; } + +#shop-subtitle { + text-align: center; + font-family: "Cinzel", serif; + font-size: 14px; + color: #5c3b20; + padding: 14px 24px 0; + flex-shrink: 0; +} + +#shop-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + padding: 20px 30px; +} + +.shop-loading { + grid-column: 1 / -1; + text-align: center; + font-family: "Cinzel", serif; + color: #8b6a3c; + padding: 40px; +} + +/* Shop Karte */ +.shop-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 20px 14px 14px; + background: linear-gradient(145deg, rgba(58,40,16,0.85), rgba(26,15,4,0.85)); + border: 2px solid #8b6a3c; + border-radius: 12px; + cursor: pointer; + transition: 0.2s; + box-shadow: 0 4px 16px rgba(0,0,0,0.5); +} +.shop-card:hover { + border-color: #f0d060; + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0,0,0,0.7), 0 0 14px rgba(200,160,60,0.3); +} + +.shop-badge { + position: absolute; + top: -10px; right: -10px; + background: linear-gradient(#c8200a, #8b1006); + border: 2px solid #ff6040; + border-radius: 20px; + font-family: "Cinzel", serif; + font-size: 10px; + font-weight: bold; + color: #fff; + padding: 3px 8px; + white-space: nowrap; +} + +.shop-gem-icon { + font-size: 42px; + filter: drop-shadow(0 2px 6px rgba(0,100,255,0.5)); +} + +.shop-package-name { + font-family: "Tangerine", serif; + font-size: 28px; + color: #f0d9a6; + text-shadow: 0 1px 4px black; +} + +.shop-gems-amount { + font-family: "Cinzel", serif; + font-size: 22px; + font-weight: bold; + color: #f0d060; +} + +.shop-gems-label { + font-family: "Cinzel", serif; + font-size: 11px; + color: #a08060; + text-transform: uppercase; + letter-spacing: 1px; +} + +.shop-breakdown { + font-family: "Cinzel", serif; + font-size: 11px; + color: #c8a86a; +} + +.shop-price-per { + font-family: "Cinzel", serif; + font-size: 10px; + color: #8b6a3c; +} + +.shop-buy-btn { + margin-top: 8px; + width: 100%; + padding: 10px; + background: linear-gradient(#6b4b2a, #3c2414); + border: 2px solid #f0d060; + border-radius: 8px; + color: #f0d9a6; + font-family: "Cinzel", serif; + font-size: 15px; + font-weight: bold; + cursor: pointer; + transition: 0.2s; +} +.shop-buy-btn:hover { + background: linear-gradient(#8b6b3a, #5c3a1a); + color: #fff; + box-shadow: 0 0 12px rgba(240,208,96,0.4); +} +.shop-buy-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +#shop-footer { + text-align: center; + font-family: "Cinzel", serif; + font-size: 11px; + color: #8b6a3c; + padding: 0 24px 20px; + flex-shrink: 0; +} diff --git a/public/js/shop.js b/public/js/shop.js new file mode 100644 index 0000000..3c4a98d --- /dev/null +++ b/public/js/shop.js @@ -0,0 +1,115 @@ +/* ============================================================ + public/js/shop.js + Shop-Popup – Gems kaufen +============================================================ */ + +const shopOverlay = document.getElementById("shop-overlay"); +const shopPopup = document.getElementById("shop-popup"); +const shopClose = document.getElementById("shop-close"); +const shopGrid = document.getElementById("shop-grid"); + +/* ════════════════════════════════════════════ + Shop öffnen +════════════════════════════════════════════ */ +export async function openShop() { + shopOverlay.classList.add("active"); + shopPopup.classList.add("active"); + await loadPackages(); +} + +/* ════════════════════════════════════════════ + Shop schließen +════════════════════════════════════════════ */ +function closeShop() { + shopOverlay.classList.remove("active"); + shopPopup.classList.remove("active"); +} + +shopClose?.addEventListener("click", closeShop); +shopOverlay?.addEventListener("click", closeShop); +document.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeShop(); +}); + +/* ════════════════════════════════════════════ + Pakete laden +════════════════════════════════════════════ */ +async function loadPackages() { + shopGrid.innerHTML = `
Lade Angebote...
`; + + try { + const res = await fetch("/api/shop/packages"); + const pkgs = await res.json(); + renderPackages(pkgs); + } catch { + shopGrid.innerHTML = `
Fehler beim Laden.
`; + } +} + +/* ════════════════════════════════════════════ + Pakete rendern +════════════════════════════════════════════ */ +function renderPackages(pkgs) { + shopGrid.innerHTML = pkgs.map(pkg => { + const total = pkg.gems + pkg.bonus; + const priceEur = (pkg.price / 100).toFixed(2).replace(".", ","); + const hasBonus = pkg.bonus > 0; + const perGem = (pkg.price / total).toFixed(1); + + return ` +
+ ${hasBonus ? `
+${pkg.bonus} Bonus!
` : ""} +
💠
+
${pkg.name}
+
${total.toLocaleString("de-DE")}
+
Gems
+ ${hasBonus ? `
${pkg.gems} + ${pkg.bonus} Bonus
` : ""} +
~ ${perGem}¢ pro Gem
+ +
`; + }).join(""); + + shopGrid.querySelectorAll(".shop-buy-btn").forEach(btn => { + btn.addEventListener("click", () => checkout(btn.dataset.id, btn)); + }); +} + +/* ════════════════════════════════════════════ + Checkout starten +════════════════════════════════════════════ */ +async function checkout(packageId, btn) { + btn.disabled = true; + btn.textContent = "Weiterleiten..."; + + try { + const res = await fetch("/api/shop/checkout", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ packageId }), + }); + const data = await res.json(); + + if (!res.ok || !data.url) { + alert(data.error || "Fehler beim Starten der Zahlung."); + btn.disabled = false; + btn.textContent = getPrice(packageId); + return; + } + + window.location.href = data.url; + } catch { + alert("Verbindungsfehler."); + btn.disabled = false; + btn.textContent = "Fehler"; + } +} + +function getPrice(id) { + const prices = { + starter: "1,99 €", abenteurer: "7,99 €", + ritter: "14,99 €", fuerst: "34,99 €", koenig: "59,99 €" + }; + return prices[id] || "Kaufen"; +} diff --git a/routes/shop.route.js b/routes/shop.route.js new file mode 100644 index 0000000..bff50bb --- /dev/null +++ b/routes/shop.route.js @@ -0,0 +1,142 @@ +/* ============================================================ + routes/shop.route.js + Stripe Shop – Gems kaufen + + .env benötigt: + STRIPE_SECRET_KEY=sk_live_... + STRIPE_WEBHOOK_SECRET=whsec_... + APP_URL=https://spiel.dynastyofknights.com +============================================================ */ + +const express = require("express"); +const router = express.Router(); +const db = require("../database/database"); +const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); + +function requireLogin(req, res, next) { + if (!req.session?.user) return res.status(401).json({ error: "Nicht eingeloggt" }); + next(); +} + +/* ════════════════════════════════════════════ + Shop-Pakete (Preis in Cent!) +════════════════════════════════════════════ */ +const PACKAGES = [ + { id: "starter", name: "Starter", gems: 100, price: 199, bonus: 0 }, + { id: "abenteurer", name: "Abenteurer", gems: 500, price: 799, bonus: 50 }, + { id: "ritter", name: "Ritter", gems: 1000, price: 1499, bonus: 150 }, + { id: "fuerst", name: "Fürst", gems: 2500, price: 3499, bonus: 500 }, + { id: "koenig", name: "König", gems: 5000, price: 5999, bonus: 1500 }, +]; + +/* ════════════════════════════════════════════ + GET /api/shop/packages + Pakete für Frontend laden +════════════════════════════════════════════ */ +router.get("/shop/packages", requireLogin, (req, res) => { + res.json(PACKAGES); +}); + +/* ════════════════════════════════════════════ + POST /api/shop/checkout + Stripe Checkout Session erstellen + Body: { packageId: "ritter" } +════════════════════════════════════════════ */ +router.post("/shop/checkout", requireLogin, async (req, res) => { + const userId = req.session.user.id; + const { packageId } = req.body; + + const pkg = PACKAGES.find(p => p.id === packageId); + if (!pkg) return res.status(400).json({ error: "Paket nicht gefunden." }); + + try { + const session = await stripe.checkout.sessions.create({ + payment_method_types: ["card", "paypal"], + line_items: [{ + price_data: { + currency: "eur", + product_data: { + name: `${pkg.name} Paket – ${pkg.gems + pkg.bonus} 💠 Gems`, + description: pkg.bonus > 0 + ? `${pkg.gems} Gems + ${pkg.bonus} Bonus-Gems` + : `${pkg.gems} Gems`, + images: [`${process.env.APP_URL}/images/items/blauer-cristal.png`], + }, + unit_amount: pkg.price, + }, + quantity: 1, + }], + mode: "payment", + success_url: `${process.env.APP_URL}/launcher?payment=success`, + cancel_url: `${process.env.APP_URL}/launcher?payment=cancel`, + metadata: { + userId: String(userId), + packageId: pkg.id, + gems: String(pkg.gems + pkg.bonus), + }, + }); + + res.json({ url: session.url }); + } catch (err) { + console.error("Stripe Fehler:", err); + res.status(500).json({ error: "Zahlung konnte nicht gestartet werden." }); + } +}); + +/* ════════════════════════════════════════════ + POST /api/shop/webhook + Stripe Webhook – Zahlung bestätigt + WICHTIG: Muss VOR express.json() registriert + werden → raw body nötig! +════════════════════════════════════════════ */ +router.post( + "/shop/webhook", + express.raw({ type: "application/json" }), + async (req, res) => { + const sig = req.headers["stripe-signature"]; + + let event; + try { + event = stripe.webhooks.constructEvent( + req.body, + sig, + process.env.STRIPE_WEBHOOK_SECRET + ); + } catch (err) { + console.error("Webhook Signatur-Fehler:", err.message); + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + if (event.type === "checkout.session.completed") { + const session = event.data.object; + const userId = parseInt(session.metadata.userId); + const gems = parseInt(session.metadata.gems); + const packageId = session.metadata.packageId; + + try { + /* Gems gutschreiben */ + await db.query( + "UPDATE account_currency SET gems = gems + ? WHERE account_id = ?", + [gems, userId] + ); + + /* Kauf protokollieren */ + await db.query( + `INSERT INTO shop_purchases + (user_id, package_id, gems, stripe_session_id, created_at) + VALUES (?, ?, ?, ?, NOW())`, + [userId, packageId, gems, session.id] + ); + + console.log(`✅ Shop: User ${userId} erhält ${gems} Gems (${packageId})`); + } catch (err) { + console.error("Gems gutschreiben Fehler:", err); + return res.status(500).end(); + } + } + + res.json({ received: true }); + } +); + +module.exports = { router, PACKAGES }; diff --git a/views/launcher.ejs b/views/launcher.ejs index 4e08c0d..3064ca2 100644 --- a/views/launcher.ejs +++ b/views/launcher.ejs @@ -667,7 +667,7 @@ 🪙 0 - +
@@ -892,6 +892,22 @@
+ +
+
+
+ 💠 Gems kaufen + +
+
Unterstütze das Spiel und erhalte wertvolle Gems!
+
+ +
+
@@ -1022,7 +1038,23 @@