diff --git a/app.js b/app.js index 8354214..bdf8275 100644 --- a/app.js +++ b/app.js @@ -21,6 +21,7 @@ const avatar = require("./routes/avatar"); const equip = require("./routes/equip"); const equipment = require("./routes/equipment"); const blackmarket = require("./routes/blackmarket"); +const mineRoute = require("./routes/mine_route"); const compression = require("compression"); @@ -208,6 +209,7 @@ app.use("/api/avatar", avatar); app.use("/api/equip", equip); app.use("/api/equipment", equipment); app.use("/api/blackmarket", blackmarket); +app.use("/api/mine", mineRoute); /* ======================== 404 Handler diff --git a/public/css/mine.css b/public/css/mine.css new file mode 100644 index 0000000..2640d16 --- /dev/null +++ b/public/css/mine.css @@ -0,0 +1,190 @@ +/* ================================================ + mine.css – Mine Gebäude UI + Passt zum bestehenden Design aus building.css + und dem qm-popup / game-notification System +================================================ */ + +/* ── Panel-Wrapper ─────────────────────────────── */ +.mine-panel { + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 0; +} + +/* ── Header-Zeile: Level + Zyklusinfo ──────────── */ +.mine-header-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.mine-level-badge { + background: linear-gradient(135deg, #3a2200, #6a3c00); + border: 1px solid #c8952a; + border-radius: 20px; + padding: 4px 12px; + color: #f0c84a; + font-size: 12px; + font-weight: bold; + letter-spacing: 0.04em; +} + +.mine-cycles { + font-size: 11px; + color: #a07830; + font-style: italic; +} + +/* ── Trennlinie ────────────────────────────────── */ +.mine-divider { + height: 1px; + background: linear-gradient(90deg, transparent, #7a5a1a, transparent); + margin: 10px 0; +} + +/* ── Abschnitt-Titel ───────────────────────────── */ +.mine-section-title { + font-size: 11px; + color: #a07830; + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 0 0 8px 0; +} + +/* ── Ressourcen-Liste ──────────────────────────── */ +.mine-resources { + display: flex; + flex-direction: column; + gap: 6px; +} + +.mine-resource-row { + display: flex; + align-items: center; + gap: 10px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(122, 90, 26, 0.3); + border-radius: 6px; + padding: 7px 12px; + transition: border-color 0.2s, background 0.2s; +} + +.mine-resource-row.mine-resource-ready { + border-color: rgba(200, 149, 42, 0.55); + background: rgba(200, 149, 42, 0.07); +} + +.mine-resource-icon { + font-size: 18px; + line-height: 1; + flex-shrink: 0; +} + +.mine-resource-label { + flex: 1; + font-size: 13px; + color: #d4b870; +} + +.mine-resource-amount { + font-size: 15px; + font-weight: bold; + color: #f0c84a; + min-width: 32px; + text-align: right; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); +} + +/* ── Timer-Zeile ───────────────────────────────── */ +.mine-timer-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2px 0; +} + +.mine-timer-label { + font-size: 12px; + color: #a07830; +} + +.mine-timer { + font-size: 13px; + font-family: monospace; + color: #f0c84a; + background: rgba(0, 0, 0, 0.35); + border: 1px solid #7a5a1a; + border-radius: 5px; + padding: 3px 10px; + letter-spacing: 0.05em; +} + +/* ── Aktions-Bereich ───────────────────────────── */ +.mine-actions { + margin-top: 14px; + display: flex; + justify-content: center; +} + +/* ── Abholen-Button (wie notification-btn) ─────── */ +.mine-btn-collect { + background: linear-gradient(180deg, #c8952a 0%, #7a5310 100%); + color: #fff8e0; + border: 1px solid #f0c84a; + border-radius: 6px; + padding: 9px 28px; + font-size: 13px; + font-family: sans-serif; + font-weight: bold; + cursor: pointer; + letter-spacing: 0.04em; + transition: filter 0.15s, transform 0.1s; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + width: 100%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); +} + +.mine-btn-collect:hover:not(:disabled) { + filter: brightness(1.2); + transform: translateY(-1px); +} + +.mine-btn-collect:active:not(:disabled) { + transform: translateY(0); + filter: brightness(0.95); +} + +.mine-btn-collect.mine-btn-disabled, +.mine-btn-collect:disabled { + background: linear-gradient(180deg, #3a3020 0%, #2a2010 100%); + border-color: #5a4a1a; + color: #7a6a3a; + cursor: not-allowed; + box-shadow: none; + transform: none; + filter: none; +} + +/* ── Lade- & Fehlerzustände ────────────────────── */ +.mine-loading { + padding: 24px; + text-align: center; + color: #a07830; + font-size: 13px; + letter-spacing: 0.04em; + animation: minePulse 1.2s ease-in-out infinite; +} + +@keyframes minePulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + +.mine-error { + padding: 16px; + text-align: center; + color: #c84040; + font-size: 12px; +} diff --git a/public/js/buildings/mine.js b/public/js/buildings/mine.js new file mode 100644 index 0000000..8946761 --- /dev/null +++ b/public/js/buildings/mine.js @@ -0,0 +1,207 @@ +/* ================================ + Mine – Frontend + Lädt den Aktionen-Tab mit + Produktionsanzeige + Abholen-Button +================================ */ + +/* showNotification() kommt aus dem globalen Scope (launcher.ejs) */ + +export async function loadMine(buildingId) { + const actionsTab = document.getElementById("tab-actions"); + if (!actionsTab) return; + + actionsTab.innerHTML = `
⛏️ Lade Mineninfo...
`; + + await renderMineStatus(buildingId); +} + +/* ───────────────────────────────────────── + Status rendern +───────────────────────────────────────── */ +async function renderMineStatus(buildingId) { + const actionsTab = document.getElementById("tab-actions"); + + try { + const res = await fetch(`/api/mine/${buildingId}/status`); + if (!res.ok) throw new Error("API Fehler"); + const data = await res.json(); + + if (data.error) { + actionsTab.innerHTML = `

⚠️ ${data.error}

`; + return; + } + + const minutesLeft = Math.floor(data.next_cycle_in_seconds / 60); + const secondsLeft = data.next_cycle_in_seconds % 60; + + /* Ressourcen-Zeilen */ + const resourceRows = data.available + .map((r) => { + const icon = resourceIcon(r.resource); + const label = resourceLabel(r.resource); + const ready = data.ready; + return ` +
+ ${icon} + ${label} + ${r.amount} +
`; + }) + .join(""); + + actionsTab.innerHTML = ` +
+ +
+ ⛏️ Level ${data.level} + ${data.cycles > 0 ? `${data.cycles}× Zyklus abgeschlossen` : "Läuft..."} +
+ +
+ +

Abgebaut

+
+ ${resourceRows} +
+ +
+ +
+ Nächster Zyklus in + + ${minutesLeft}m ${secondsLeft}s + +
+ +
+ +
+ +
`; + + startCountdown(buildingId); + + } catch (err) { + console.error("Mine Fehler:", err); + actionsTab.innerHTML = `

⚠️ Fehler beim Laden der Mineninfo.

`; + } +} + +/* ───────────────────────────────────────── + Abholen-Klick +───────────────────────────────────────── */ +document.addEventListener("click", async (e) => { + const btn = e.target.closest("#mine-collect-btn"); + if (!btn || btn.disabled) return; + + const buildingId = btn.dataset.building; + + btn.disabled = true; + btn.textContent = "⏳ Wird abgeholt..."; + + try { + const res = await fetch(`/api/mine/${buildingId}/collect`, { method: "POST" }); + if (!res.ok) throw new Error("API Fehler"); + const data = await res.json(); + + if (data.error) { + showNotification( + data.ready_in_display + ? `Die Mine ist noch nicht bereit.\nBereit in: ${data.ready_in_display}` + : data.error, + "Mine", + "⛏️" + ); + await renderMineStatus(buildingId); + return; + } + + /* Erfolg – was wurde abgeholt? */ + const lines = data.collected + .map((c) => `${resourceIcon(c.resource)} ${resourceLabel(c.resource)}: +${c.amount}`) + .join("\n"); + + showNotification( + `Erfolgreich abgeholt!\n\n${lines}`, + "Mine", + "⛏️" + ); + + await renderMineStatus(buildingId); + + } catch (err) { + console.error("Abholen Fehler:", err); + showNotification( + "Fehler beim Abholen. Bitte erneut versuchen.", + "Fehler", + "⚠️" + ); + await renderMineStatus(buildingId); + } +}); + +/* ───────────────────────────────────────── + Countdown-Timer (live) +───────────────────────────────────────── */ +let countdownInterval = null; + +function startCountdown(buildingId) { + if (countdownInterval) clearInterval(countdownInterval); + + countdownInterval = setInterval(() => { + const el = document.getElementById("mine-countdown"); + if (!el) { + clearInterval(countdownInterval); + return; + } + + let secs = parseInt(el.dataset.seconds, 10) - 1; + if (secs < 0) secs = 0; + el.dataset.seconds = secs; + + const m = Math.floor(secs / 60); + const s = secs % 60; + el.textContent = `${m}m ${s}s`; + + /* Zyklus abgeschlossen → UI neu laden */ + if (secs === 0) { + clearInterval(countdownInterval); + renderMineStatus(buildingId); + } + }, 1000); +} + +/* ───────────────────────────────────────── + Hilfsfunktionen +───────────────────────────────────────── */ +function resourceIcon(resource) { + const map = { + gold: "🪙", + copper: "🟤", + silver: "⚪", + iron: "⚙️", + stone: "🪨", + wood: "🪵", + }; + return map[resource] ?? "📦"; +} + +function resourceLabel(resource) { + const map = { + gold: "Gold", + copper: "Kupfer", + silver: "Silber", + iron: "Eisen", + stone: "Stein", + wood: "Holz", + }; + return map[resource] ?? resource; +} diff --git a/public/js/map-ui.js b/public/js/map-ui.js index c9ecdb7..b01ed06 100644 --- a/public/js/map-ui.js +++ b/public/js/map-ui.js @@ -1,5 +1,6 @@ import { loadWohnhaus } from "./buildings/wohnhaus.js"; import { loadSchwarzmarkt } from "./buildings/schwarzmarkt.js"; +import { loadMine } from "./buildings/mine.js"; const popup = document.getElementById("building-popup"); const title = document.getElementById("popup-title"); const tooltip = document.getElementById("map-tooltip"); @@ -9,6 +10,7 @@ const tooltipCache = {}; const buildingModules = { 11: loadWohnhaus, 12: loadSchwarzmarkt, + 10: loadMine, }; /* ================================ diff --git a/routes/mine_route.js b/routes/mine_route.js new file mode 100644 index 0000000..1d24c33 --- /dev/null +++ b/routes/mine_route.js @@ -0,0 +1,163 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../database/database"); +const auth = require("../middleware/auth"); + +/* ───────────────────────────────────────── + GET /api/mine/:buildingId/status + Liefert Level, verfügbare Ressourcen + und Countdown bis zum nächsten Zyklus +───────────────────────────────────────── */ +router.get("/:buildingId/status", auth, async (req, res) => { + const userId = req.session.user.id; + const buildingId = req.params.buildingId; + + try { + const [rows] = await db.query( + ` + SELECT + ub.id AS user_building_id, + ub.level, + bp.resource, + bp.amount, + bp.cycle_seconds, + COALESCE(bct.last_collected, NOW()) AS last_collected + FROM user_buildings ub + JOIN building_production bp + ON bp.building_id = ub.building_id + AND bp.level = ub.level + LEFT JOIN building_collect_timer bct + ON bct.user_building_id = ub.id + WHERE ub.user_id = ? + AND ub.building_id = ? + `, + [userId, buildingId] + ); + + if (!rows.length) { + return res.status(404).json({ error: "Gebäude nicht gefunden" }); + } + + const { cycle_seconds, last_collected, level } = rows[0]; + + const elapsed = Math.floor( + (Date.now() - new Date(last_collected).getTime()) / 1000 + ); + const cycles = Math.floor(elapsed / cycle_seconds); + const nextIn = cycle_seconds - (elapsed % cycle_seconds); + + const available = rows.map((r) => ({ + resource: r.resource, + amount: r.amount * cycles, + })); + + res.json({ + level, + cycles, + ready: cycles > 0, + available, + last_collected, + next_cycle_in_seconds: nextIn, + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +/* ───────────────────────────────────────── + POST /api/mine/:buildingId/collect + Schreibt Ressourcen gut, setzt Timer +───────────────────────────────────────── */ +router.post("/:buildingId/collect", auth, async (req, res) => { + const userId = req.session.user.id; + const buildingId = req.params.buildingId; + + try { + const [rows] = await db.query( + ` + SELECT + ub.id AS user_building_id, + ub.level, + bp.resource, + bp.amount, + bp.cycle_seconds, + COALESCE(bct.last_collected, NOW()) AS last_collected + FROM user_buildings ub + JOIN building_production bp + ON bp.building_id = ub.building_id + AND bp.level = ub.level + LEFT JOIN building_collect_timer bct + ON bct.user_building_id = ub.id + WHERE ub.user_id = ? + AND ub.building_id = ? + `, + [userId, buildingId] + ); + + if (!rows.length) { + return res.status(404).json({ error: "Gebäude nicht gefunden" }); + } + + const { user_building_id, cycle_seconds, last_collected } = rows[0]; + + const elapsed = Math.floor( + (Date.now() - new Date(last_collected).getTime()) / 1000 + ); + const cycles = Math.floor(elapsed / cycle_seconds); + + if (cycles < 1) { + const waitSeconds = cycle_seconds - elapsed; + const minutes = Math.floor(waitSeconds / 60); + const seconds = waitSeconds % 60; + return res.json({ + error: "Noch nichts bereit", + ready_in_seconds: waitSeconds, + ready_in_display: `${minutes}m ${seconds}s`, + }); + } + + // Ressourcen gutschreiben + for (const row of rows) { + const toAdd = row.amount * cycles; + await db.query( + ` + INSERT INTO account_currency (account_id, \`${row.resource}\`) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE \`${row.resource}\` = \`${row.resource}\` + ? + `, + [userId, toAdd, toAdd] + ); + } + + // Timer vorsetzen — Rest-Sekunden bleiben erhalten, kein Verlust + const newLastCollected = new Date( + new Date(last_collected).getTime() + cycles * cycle_seconds * 1000 + ); + + await db.query( + ` + INSERT INTO building_collect_timer (user_building_id, last_collected) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE last_collected = ? + `, + [user_building_id, newLastCollected, newLastCollected] + ); + + const collected = rows.map((r) => ({ + resource: r.resource, + amount: r.amount * cycles, + })); + + res.json({ + success: true, + cycles, + collected, + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + +module.exports = router; diff --git a/views/launcher.ejs b/views/launcher.ejs index 6ce4010..c06db3c 100644 --- a/views/launcher.ejs +++ b/views/launcher.ejs @@ -12,6 +12,7 @@ +