diff --git a/public/css/mine.css b/public/css/mine.css index 9925330..4e9e22c 100644 --- a/public/css/mine.css +++ b/public/css/mine.css @@ -1,7 +1,6 @@ /* ================================================ mine.css – Mine Gebäude UI Passt zum bestehenden Design aus building.css - und dem qm-popup / game-notification System ================================================ */ /* ── Panel-Wrapper ─────────────────────────────── */ @@ -12,7 +11,7 @@ gap: 0; } -/* ── Header-Zeile: Level + Zyklusinfo ──────────── */ +/* ── Header ────────────────────────────────────── */ .mine-header-row { display: flex; align-items: center; @@ -53,7 +52,27 @@ margin: 0 0 8px 0; } -/* ── Ressourcen-Auswahl ────────────────────────── */ +.mine-queue-count { + color: #c8952a; + font-weight: normal; + text-transform: none; + letter-spacing: 0; +} + +/* ── Hinweistext ───────────────────────────────── */ +.mine-hint { + font-size: 11px; + color: #806040; + margin: 0 0 8px 0; + line-height: 1.4; +} + +.mine-hint-center { + text-align: center; + margin-top: 6px; +} + +/* ── Ressourcen-Auswahl Raster ─────────────────── */ .mine-res-selector { display: grid; grid-template-columns: 1fr 1fr; @@ -118,17 +137,11 @@ } /* ── Aktuelle Produktion ───────────────────────── */ -.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); + background: rgba(255,255,255,0.03); border: 1px solid rgba(122, 90, 26, 0.3); border-radius: 6px; padding: 7px 12px; @@ -149,7 +162,6 @@ justify-content: center; } -/* Ressource Icons – einheitliche Größe */ .mine-resource-icon-gold, .mine-resource-icon-iron, .mine-resource-icon-wood, @@ -235,96 +247,113 @@ animation: minePulse 1s ease-in-out infinite; } -/* ── Schleifen-Bereich ─────────────────────────── */ -.mine-loop-row { +/* ── Warteschlangen-Slots ──────────────────────── */ +.mine-queue-slot { display: flex; align-items: center; - justify-content: space-between; - margin-bottom: 8px; -} - -.mine-loop-dots { - display: flex; - gap: 6px; - align-items: center; -} - -.mine-loop-dot { - width: 14px; - height: 14px; - border-radius: 50%; - background: rgba(255,255,255,0.08); - border: 1px solid rgba(122, 90, 26, 0.5); - transition: background 0.3s, border-color 0.3s; -} - -.mine-loop-dot-active { - background: linear-gradient(135deg, #a030f0, #6010c0); - border-color: #c060ff; - box-shadow: 0 0 6px rgba(160, 48, 240, 0.6); -} - -.mine-loop-info { + gap: 8px; + border-radius: 6px; + padding: 6px 10px; + margin-bottom: 5px; font-size: 12px; - color: #a07830; } -.mine-loop-maxed { - color: #f0c84a; +.mine-queue-slot-filled { + background: rgba(160, 48, 240, 0.08); + border: 1px solid rgba(160, 48, 240, 0.35); +} + +.mine-queue-pos { + font-size: 11px; + color: #806080; + min-width: 16px; +} + +.mine-queue-icon { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; +} + +.mine-queue-icon img { + width: 22px; + height: 22px; + object-fit: contain; + filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6)); + display: block; +} + +.mine-queue-label { + flex: 1; + color: #d0a0ff; font-weight: bold; } -.mine-btn-loop { +.mine-queue-dur { + font-size: 11px; + color: #806080; +} + +/* ── Schleife hinzufügen ───────────────────────── */ +.mine-queue-add { + margin-bottom: 2px; +} + +.mine-btn-add-loop { width: 100%; display: flex; align-items: center; justify-content: center; gap: 6px; - background: linear-gradient(180deg, #3a1060 0%, #200840 100%); - color: #d090ff; - border: 1px solid #8040c0; + background: linear-gradient(180deg, #2a1040 0%, #180828 100%); + color: #c080ff; + border: 1px dashed #6030a0; border-radius: 6px; - padding: 8px 16px; + padding: 8px 14px; font-size: 12px; font-family: sans-serif; font-weight: bold; cursor: pointer; - letter-spacing: 0.03em; - transition: filter 0.15s, transform 0.1s; - box-shadow: 0 2px 6px rgba(0,0,0,0.4); + letter-spacing: 0.02em; + transition: border-color 0.2s, background 0.2s, transform 0.1s; } -.mine-btn-loop:hover:not(:disabled) { - filter: brightness(1.25); +.mine-btn-add-loop:hover:not(:disabled) { + border-color: #a060e0; + background: linear-gradient(180deg, #3a1860 0%, #280a40 100%); transform: translateY(-1px); } -.mine-btn-loop:active:not(:disabled) { - transform: translateY(0); - filter: brightness(0.9); +.mine-btn-add-loop-open { + border-style: solid; + border-color: #a060e0; + background: linear-gradient(180deg, #3a1860 0%, #280a40 100%); } -.mine-btn-loop.mine-btn-disabled, -.mine-btn-loop:disabled { - background: linear-gradient(180deg, #1a1020 0%, #100810 100%); - border-color: #3a2050; - color: #604080; +.mine-btn-add-loop.mine-btn-disabled, +.mine-btn-add-loop:disabled { + background: linear-gradient(180deg, #1a1020 0%, #100818 100%); + border-color: #301850; + color: #503060; cursor: not-allowed; - box-shadow: none; transform: none; - filter: none; +} + +/* ── Picker im Loop-Bereich ────────────────────── */ +.mine-loop-picker { + margin-top: 8px; + padding: 10px; + background: rgba(100, 30, 160, 0.07); + border: 1px solid rgba(100, 30, 160, 0.25); + border-radius: 6px; } .mine-loop-gem-icon { font-size: 14px; } -.mine-loop-no-gems { - font-size: 10px; - color: #804060; - font-weight: normal; -} - /* ── Aktions-Bereich ───────────────────────────── */ .mine-actions { margin-top: 4px; diff --git a/public/js/buildings/mine.js b/public/js/buildings/mine.js index e16eb2c..f5d80e8 100644 --- a/public/js/buildings/mine.js +++ b/public/js/buildings/mine.js @@ -28,12 +28,13 @@ async function renderMineStatus(buildingId) { "
" + renderHeader(data) + renderDivider() + - renderResourceSelector(data, buildingId) + - (data.selected_resource ? renderProductionSection(data) : "") + - (data.selected_resource ? renderDivider() : "") + - (data.selected_resource ? renderLoopSection(data, buildingId) : "") + - (data.selected_resource ? renderDivider() : "") + - (data.selected_resource ? renderCollectSection(data, buildingId) : "") + + + // Ressource wählen (nur wenn noch keine aktive Session) + (!data.selected_resource + ? renderResourceSelector(data, buildingId) + : renderActiveSession(data, buildingId) + ) + + "
"; if (data.selected_resource) { @@ -47,18 +48,14 @@ async function renderMineStatus(buildingId) { } /* ───────────────────────────────────────────────── - Abschnitt: Header (Level + Zyklusinfo) + Header ───────────────────────────────────────────────── */ function renderHeader(data) { let cycleText = "Keine Ressource gewählt"; if (data.selected_resource) { - if (data.is_full) { - cycleText = "Voll – bitte abholen!"; - } else if (data.cycles > 0) { - cycleText = data.cycles + "x Zyklus abgeschlossen"; - } else { - cycleText = "Läuft..."; - } + if (data.is_full) cycleText = "Voll – bitte abholen!"; + else if (data.cycles > 0) cycleText = data.cycles + "x Zyklus abgeschlossen"; + else cycleText = "Läuft..."; } return ( "
" + @@ -69,18 +66,133 @@ function renderHeader(data) { } /* ───────────────────────────────────────────────── - Abschnitt: Ressourcen-Auswahl + Ressourcen-Auswahl (Startscreen ohne aktive Session) ───────────────────────────────────────────────── */ function renderResourceSelector(data, buildingId) { - const resources = data.production; // [{resource, amount}] + return ( + "

Ressource wählen

" + + "

Wähle eine Ressource für die nächsten " + data.session_hours + "h.

" + + renderResourceGrid(data.production, null, buildingId, "select") + ); +} - const buttons = resources.map(r => { - const isSelected = data.selected_resource === r.resource; +/* ───────────────────────────────────────────────── + Aktive Session (mit Timer, Queue, Collect-Button) +───────────────────────────────────────────────── */ +function renderActiveSession(data, buildingId) { + return ( + // Aktuelle Produktion + renderCurrentProduction(data) + + renderDivider() + + + // Warteschlangen-Bereich + renderQueueSection(data, buildingId) + + renderDivider() + + + // Abholen-Button + renderCollectSection(data, buildingId) + ); +} + +/* ───────────────────────────────────────────────── + Aktuelle Produktion +───────────────────────────────────────────────── */ +function renderCurrentProduction(data) { + const minutesLeft = Math.floor(data.next_cycle_in_seconds / 60); + const secondsLeft = data.next_cycle_in_seconds % 60; + const progressPct = Math.min(100, Math.round((data.cycles / data.max_cycles) * 100)); + + return ( + "

Aktuelle Session – " + data.session_hours + "h

" + + "
" + + "" + resourceIcon(data.selected_resource) + "" + + "" + resourceLabel(data.selected_resource) + "" + + "" + data.available_amount + "" + + "
" + + "
" + + "
" + + "
" + + "
" + + "" + + (data.is_full ? "Zeit abgelaufen!" : "Nächster Zyklus in") + + "" + + (data.is_full + ? "VOLL" + : "" + + minutesLeft + "m " + secondsLeft + "s" + ) + + "
" + ); +} + +/* ───────────────────────────────────────────────── + Warteschlangen-Bereich + – zeigt belegte Slots als Ressourcen-Badges + – zeigt freie Slots als aufklappbare Auswahl +───────────────────────────────────────────────── */ +function renderQueueSection(data, buildingId) { + const { loop_queue, loop_slots_free, loop_slots_used, + loop_cost_gems, can_afford_loop, player_gems } = data; + + let html = + "

Warteschlange" + + " (" + loop_slots_used + "/4)

"; + + // Belegte Slots + loop_queue.forEach((res, i) => { + html += + "
" + + "" + (i + 1) + "." + + "" + resourceIcon(res) + "" + + "" + resourceLabel(res) + "" + + "" + data.session_hours + "h" + + "
"; + }); + + // Freie Slots (kaufbar) + if (loop_slots_free > 0) { + const nextPos = loop_slots_used + 1; + html += + "
" + + "" + + // Resource-Picker (standardmäßig versteckt) + "" + + "
"; + } + + if (loop_slots_free === 0) { + html += "

Warteschlange voll – 4 Schleifen aktiv.

"; + } + + return html; +} + +/* ───────────────────────────────────────────────── + Ressourcen-Raster (wiederverwendbar für select + loop) + mode: "select" | "loop" +───────────────────────────────────────────────── */ +function renderResourceGrid(production, activeResource, buildingId, mode) { + const buttons = production.map(r => { + const isActive = r.resource === activeResource; return ( - "" - : "" - ) - ); -} - -/* ───────────────────────────────────────────────── - Abschnitt: Abholen-Button + Collect-Button ───────────────────────────────────────────────── */ function renderCollectSection(data, buildingId) { return ( @@ -195,20 +224,39 @@ function renderDivider() { } /* ───────────────────────────────────────────────── - Event-Delegation: Ressource wählen + Event: Toggle Schleifenpicker +───────────────────────────────────────────────── */ +document.addEventListener("click", (e) => { + const btn = e.target.closest("#mine-queue-toggle"); + if (!btn || btn.disabled) return; + + const picker = document.getElementById("mine-loop-picker"); + if (!picker) return; + + const isOpen = picker.style.display !== "none"; + picker.style.display = isOpen ? "none" : "block"; + btn.classList.toggle("mine-btn-add-loop-open", !isOpen); +}); + +/* ───────────────────────────────────────────────── + Event: Ressource wählen (select) oder Schleife buchen (loop) ───────────────────────────────────────────────── */ document.addEventListener("click", async (e) => { const btn = e.target.closest(".mine-res-btn"); - if (!btn || btn.classList.contains("mine-res-btn-active")) return; + if (!btn || btn.disabled || btn.classList.contains("mine-res-btn-active")) return; const resource = btn.dataset.resource; const buildingId = btn.dataset.building; + const mode = btn.dataset.mode; // "select" | "loop" - // Alle Buttons kurz deaktivieren - document.querySelectorAll(".mine-res-btn").forEach(b => b.disabled = true); + document.querySelectorAll(".mine-res-btn").forEach(b => (b.disabled = true)); + + const endpoint = mode === "loop" + ? "/api/mine/" + buildingId + "/loop" + : "/api/mine/" + buildingId + "/select"; try { - const res = await fetch("/api/mine/" + buildingId + "/select", { + const res = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ resource }), @@ -222,64 +270,37 @@ document.addEventListener("click", async (e) => { return; } - showNotification( - resourceLabel(resource) + " ausgewählt.\nTimer wurde zurückgesetzt.", - "Mine", "⛏️" - ); - await renderMineStatus(buildingId); - } catch (err) { - console.error("Ressource wählen Fehler:", err); - await renderMineStatus(buildingId); - } -}); - -/* ───────────────────────────────────────────────── - Event-Delegation: Schleife kaufen -───────────────────────────────────────────────── */ -document.addEventListener("click", async (e) => { - const btn = e.target.closest("#mine-loop-btn"); - if (!btn || btn.disabled) return; - - const buildingId = btn.dataset.building; - btn.disabled = true; - btn.textContent = "Kaufe..."; - - try { - const res = await fetch("/api/mine/" + buildingId + "/loop", { - method: "POST", - }); - if (!res.ok) throw new Error("API Fehler"); - const data = await res.json(); - - if (data.error) { - showNotification(data.error, "Mine", "⛏️"); - await renderMineStatus(buildingId); - return; + if (mode === "loop") { + showNotification( + "Schleife hinzugefügt!\n" + resourceLabel(resource) + " – 5h\n" + + "Noch " + data.loop_slots_free + " Slot(s) frei.", + "Mine", "💎" + ); + } else { + showNotification( + resourceLabel(resource) + " ausgewählt.\n5h Session startet jetzt.", + "Mine", "⛏️" + ); } - showNotification( - "Schleife aktiviert! +" + 5 + "h Abbauzeit.\n" + - "Noch " + data.loops_available + " Schleife(n) verfügbar.", - "Mine", "💎" - ); await renderMineStatus(buildingId); - await refreshHud(); + if (mode === "loop") await refreshHud(); } catch (err) { - console.error("Schleife kaufen Fehler:", err); + console.error("Mine Fehler:", err); await renderMineStatus(buildingId); } }); /* ───────────────────────────────────────────────── - Event-Delegation: Ressource abholen + Event: Abholen ───────────────────────────────────────────────── */ 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..."; + btn.disabled = true; + btn.textContent = "Wird abgeholt..."; try { const res = await fetch("/api/mine/" + buildingId + "/collect", { @@ -300,10 +321,14 @@ document.addEventListener("click", async (e) => { } const c = data.collected; - showNotification( - "Abgeholt!\n" + resourceLabel(c.resource) + ": +" + c.amount, - "Mine", "⛏️" - ); + let msg = "Abgeholt!\n" + resourceLabel(c.resource) + ": +" + c.amount; + if (data.next_resource) { + msg += "\n\nNächste Session: " + resourceLabel(data.next_resource) + " (5h)"; + } else { + msg += "\n\nKeine weiteren Schleifen – neue Ressource wählen."; + } + + showNotification(msg, "Mine", "⛏️"); await renderMineStatus(buildingId); await refreshHud(); } catch (err) { @@ -359,11 +384,6 @@ function resourceIcon(resource) { } function resourceLabel(resource) { - const map = { - gold: "Gold", - iron: "Eisen", - stone: "Stein", - wood: "Holz", - }; + const map = { gold: "Gold", iron: "Eisen", stone: "Stein", wood: "Holz" }; return map[resource] || resource; } diff --git a/routes/mine.route.js b/routes/mine.route.js index e653435..2032811 100644 --- a/routes/mine.route.js +++ b/routes/mine.route.js @@ -1,12 +1,11 @@ const express = require("express"); -const router = require("express").Router(); -const db = require("../database/database"); +const router = require("express").Router(); +const db = require("../database/database"); /* ── Konstanten ─────────────────────────────────── */ -const MAX_BASE_HOURS = 5; // Basis-Stunden pro Session -const MAX_LOOPS = 4; // Maximale kaufbare Schleifen -const LOOP_COST_GEMS = 10; // Juwelen pro Schleife -const LOOP_HOURS = 5; // Zusatzstunden pro Schleife +const SESSION_HOURS = 5; // Stunden pro Session / Slot +const MAX_QUEUE_SLOTS = 4; // Maximale Schleifen in der Warteschlange +const LOOP_COST_GEMS = 10; // Juwelen pro Schleife const MINE_RESOURCES = ["gold", "iron", "stone", "wood"]; /* ── Auth-Guard ─────────────────────────────────── */ @@ -19,8 +18,6 @@ function requireLogin(req, res, next) { /* ───────────────────────────────────────────────── HELPER: Timer sicherstellen - Legt beim allerersten Aufruf einen Eintrag an. - Wird durch select() und collect() zurückgesetzt. ───────────────────────────────────────────────── */ async function ensureTimer(userBuildingId) { const [[existing]] = await db.query( @@ -30,16 +27,15 @@ async function ensureTimer(userBuildingId) { if (!existing) { await db.query( `INSERT INTO building_collect_timer - (user_building_id, last_collected, selected_resource, loops_purchased) - VALUES (?, NOW(), NULL, 0)`, + (user_building_id, last_collected, selected_resource, loop_queue) + VALUES (?, NOW(), NULL, JSON_ARRAY())`, [userBuildingId] ); } } /* ───────────────────────────────────────────────── - HELPER: Produktionsdaten laden - Filtert auf die vier abbaubaren Ressourcen. + HELPER: Produktionsdaten + Timer laden ───────────────────────────────────────────────── */ async function loadMineData(userId, buildingId) { const [rows] = await db.query( @@ -51,7 +47,7 @@ async function loadMineData(userId, buildingId) { bp.cycle_seconds, bct.last_collected, bct.selected_resource, - bct.loops_purchased + bct.loop_queue FROM user_buildings ub JOIN building_production bp ON bp.building_id = ub.building_id @@ -67,12 +63,23 @@ async function loadMineData(userId, buildingId) { } /* ───────────────────────────────────────────────── - HELPER: Maximale Zyklen für diese Session - Basis 5h + je 5h pro gekaufter Schleife + HELPER: Maximale Zyklen einer 5h-Session ───────────────────────────────────────────────── */ -function calcMaxCycles(loopsPurchased, cycleSeconds) { - const maxHours = MAX_BASE_HOURS + loopsPurchased * LOOP_HOURS; - return Math.floor((maxHours * 3600) / cycleSeconds); +function calcMaxCycles(cycleSeconds) { + return Math.floor((SESSION_HOURS * 3600) / cycleSeconds); +} + +/* ───────────────────────────────────────────────── + HELPER: loop_queue sicher parsen +───────────────────────────────────────────────── */ +function parseQueue(raw) { + if (!raw) return []; + try { + const q = typeof raw === "string" ? JSON.parse(raw) : raw; + return Array.isArray(q) ? q : []; + } catch { + return []; + } } /* ───────────────────────────────────────────────── @@ -99,29 +106,25 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => { const { cycle_seconds, last_collected, - selected_resource, loops_purchased, level + selected_resource, loop_queue, level } = rows[0]; - const maxCycles = calcMaxCycles(loops_purchased, cycle_seconds); - const maxHours = MAX_BASE_HOURS + loops_purchased * LOOP_HOURS; + const queue = parseQueue(loop_queue); + const maxCycles = calcMaxCycles(cycle_seconds); - const elapsed = Math.floor((Date.now() - new Date(last_collected).getTime()) / 1000); - const rawCycles = Math.floor(elapsed / cycle_seconds); - const cycles = Math.min(rawCycles, maxCycles); - const isFull = rawCycles >= maxCycles; + const elapsed = Math.floor((Date.now() - new Date(last_collected).getTime()) / 1000); + const rawCycles = Math.floor(elapsed / cycle_seconds); + const cycles = Math.min(rawCycles, maxCycles); + const isFull = rawCycles >= maxCycles; + const nextIn = isFull ? 0 : cycle_seconds - (elapsed % cycle_seconds); - const nextIn = isFull ? 0 : cycle_seconds - (elapsed % cycle_seconds); - - // Menge der gewählten Ressource - const selectedRow = selected_resource + const selectedRow = selected_resource ? rows.find(r => r.resource === selected_resource) : null; const availableAmount = selectedRow ? selectedRow.amount * cycles : 0; - // Produktionsübersicht aller vier Ressourcen const production = rows.map(r => ({ resource: r.resource, amount: r.amount })); - // Juwelen des Spielers für Loop-Anzeige const [[currency]] = await db.query( "SELECT gems FROM account_currency WHERE account_id = ?", [userId] @@ -131,17 +134,18 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => { res.json({ level, cycles, - max_cycles: maxCycles, - max_hours: maxHours, - is_full: isFull, - ready: cycles > 0 && !!selected_resource, + max_cycles: maxCycles, + session_hours: SESSION_HOURS, + is_full: isFull, + ready: cycles > 0 && !!selected_resource, selected_resource, - loops_purchased, - loops_available: MAX_LOOPS - loops_purchased, - loop_cost_gems: LOOP_COST_GEMS, - can_afford_loop: gems >= LOOP_COST_GEMS, - player_gems: gems, - available_amount: availableAmount, + loop_queue: queue, // z.B. ["wood", "iron"] + loop_slots_used: queue.length, + loop_slots_free: MAX_QUEUE_SLOTS - queue.length, + loop_cost_gems: LOOP_COST_GEMS, + can_afford_loop: gems >= LOOP_COST_GEMS, + player_gems: gems, + available_amount: availableAmount, production, last_collected, next_cycle_in_seconds: nextIn, @@ -155,7 +159,7 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => { /* ───────────────────────────────────────────────── POST /api/mine/:buildingId/select - Ressource wählen – setzt Timer und Schleifen zurück + Erste Ressource wählen – startet Session neu ───────────────────────────────────────────────── */ router.post("/:buildingId/select", requireLogin, async (req, res) => { const userId = req.session.user.id; @@ -177,7 +181,6 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => { await ensureTimer(userBuilding.id); - // Prüfen ob Ressource in building_production vorhanden const [[prod]] = await db.query( `SELECT bp.resource FROM user_buildings ub @@ -187,13 +190,13 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => { [userBuilding.id, resource] ); if (!prod) { - return res.status(400).json({ error: "Ressource für dieses Gebäude nicht verfügbar" }); + return res.status(400).json({ error: "Ressource nicht verfügbar" }); } - // Timer zurücksetzen, Ressource setzen, Schleifen zurücksetzen + // Session neu starten, Warteschlange leeren await db.query( `UPDATE building_collect_timer - SET selected_resource = ?, last_collected = NOW(), loops_purchased = 0 + SET selected_resource = ?, last_collected = NOW(), loop_queue = JSON_ARRAY() WHERE user_building_id = ?`, [resource, userBuilding.id] ); @@ -205,9 +208,83 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => { } }); +/* ───────────────────────────────────────────────── + POST /api/mine/:buildingId/loop + Schleife mit eigener Ressourcenwahl zur Queue hinzufügen +───────────────────────────────────────────────── */ +router.post("/:buildingId/loop", requireLogin, async (req, res) => { + const userId = req.session.user.id; + const buildingId = req.params.buildingId; + const { resource } = req.body; + + if (!MINE_RESOURCES.includes(resource)) { + return res.status(400).json({ error: "Ungültige Ressource" }); + } + + try { + const [[userBuilding]] = await db.query( + "SELECT id FROM user_buildings WHERE user_id = ? AND building_id = ?", + [userId, buildingId] + ); + if (!userBuilding) { + return res.status(404).json({ error: "Gebäude nicht gefunden" }); + } + + const [[timer]] = await db.query( + "SELECT loop_queue, selected_resource FROM building_collect_timer WHERE user_building_id = ?", + [userBuilding.id] + ); + if (!timer) { + return res.status(404).json({ error: "Timer nicht gefunden" }); + } + if (!timer.selected_resource) { + return res.status(400).json({ error: "Erst eine Ressource für die aktuelle Session wählen" }); + } + + const queue = parseQueue(timer.loop_queue); + if (queue.length >= MAX_QUEUE_SLOTS) { + return res.status(400).json({ error: "Warteschlange ist voll (max. 4 Schleifen)" }); + } + + const [[currency]] = await db.query( + "SELECT gems FROM account_currency WHERE account_id = ?", + [userId] + ); + if (!currency || currency.gems < LOOP_COST_GEMS) { + return res.status(400).json({ + error: `Nicht genug Juwelen (benötigt: ${LOOP_COST_GEMS}, vorhanden: ${currency?.gems ?? 0})`, + }); + } + + // Juwelen abziehen & Ressource ans Ende der Queue hängen + await db.query( + "UPDATE account_currency SET gems = gems - ? WHERE account_id = ?", + [LOOP_COST_GEMS, userId] + ); + + queue.push(resource); + + await db.query( + "UPDATE building_collect_timer SET loop_queue = ? WHERE user_building_id = ?", + [JSON.stringify(queue), userBuilding.id] + ); + + res.json({ + success: true, + loop_queue: queue, + loop_slots_used: queue.length, + loop_slots_free: MAX_QUEUE_SLOTS - queue.length, + gems_remaining: currency.gems - LOOP_COST_GEMS, + }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "DB Fehler" }); + } +}); + /* ───────────────────────────────────────────────── POST /api/mine/:buildingId/collect - Ressourcen gutschreiben + Timer vorsetzen + Ressourcen gutschreiben + nächsten Queue-Slot starten ───────────────────────────────────────────────── */ router.post("/:buildingId/collect", requireLogin, async (req, res) => { const userId = req.session.user.id; @@ -230,14 +307,14 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => { const { user_building_id, cycle_seconds, last_collected, - selected_resource, loops_purchased + selected_resource, loop_queue } = rows[0]; if (!selected_resource) { return res.status(400).json({ error: "Keine Ressource ausgewählt" }); } - const maxCycles = calcMaxCycles(loops_purchased, cycle_seconds); + const maxCycles = calcMaxCycles(cycle_seconds); const elapsed = Math.floor((Date.now() - new Date(last_collected).getTime()) / 1000); const rawCycles = Math.floor(elapsed / cycle_seconds); const cycles = Math.min(rawCycles, maxCycles); @@ -256,8 +333,8 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => { return res.status(400).json({ error: "Ressource nicht gefunden" }); } + // Ressource gutschreiben const toAdd = selectedRow.amount * cycles; - await db.query( `UPDATE account_currency SET \`${selected_resource}\` = \`${selected_resource}\` + ? @@ -265,88 +342,30 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => { [toAdd, userId] ); - // Timer exakt vorsetzen – Restsekunden bleiben erhalten + // Queue: erstes Element wird zur neuen aktiven Session + const queue = parseQueue(loop_queue); + const nextResource = queue.length > 0 ? queue.shift() : null; + + // Timer exakt vorsetzen (Restsekunden bleiben erhalten) const newLastCollected = new Date( new Date(last_collected).getTime() + cycles * cycle_seconds * 1000 ); - // Schleifen nach dem Abholen zurücksetzen await db.query( `UPDATE building_collect_timer - SET last_collected = ?, loops_purchased = 0 + SET last_collected = ?, + selected_resource = ?, + loop_queue = ? WHERE user_building_id = ?`, - [newLastCollected, user_building_id] + [newLastCollected, nextResource, JSON.stringify(queue), user_building_id] ); res.json({ - success: true, + success: true, cycles, - collected: { resource: selected_resource, amount: toAdd }, - }); - } catch (err) { - console.error(err); - res.status(500).json({ error: "DB Fehler" }); - } -}); - -/* ───────────────────────────────────────────────── - POST /api/mine/:buildingId/loop - Schleife für 10 Juwelen kaufen (max. 4 pro Session) -───────────────────────────────────────────────── */ -router.post("/:buildingId/loop", requireLogin, async (req, res) => { - const userId = req.session.user.id; - const buildingId = req.params.buildingId; - - try { - const [[userBuilding]] = await db.query( - "SELECT id FROM user_buildings WHERE user_id = ? AND building_id = ?", - [userId, buildingId] - ); - if (!userBuilding) { - return res.status(404).json({ error: "Gebäude nicht gefunden" }); - } - - const [[timer]] = await db.query( - "SELECT loops_purchased, selected_resource FROM building_collect_timer WHERE user_building_id = ?", - [userBuilding.id] - ); - if (!timer) { - return res.status(404).json({ error: "Timer nicht gefunden" }); - } - if (!timer.selected_resource) { - return res.status(400).json({ error: "Erst eine Ressource auswählen" }); - } - if (timer.loops_purchased >= MAX_LOOPS) { - return res.status(400).json({ error: "Maximale Anzahl Schleifen bereits erreicht" }); - } - - // Juwelen prüfen - const [[currency]] = await db.query( - "SELECT gems FROM account_currency WHERE account_id = ?", - [userId] - ); - if (!currency || currency.gems < LOOP_COST_GEMS) { - return res.status(400).json({ - error: `Nicht genug Juwelen (benötigt: ${LOOP_COST_GEMS}, vorhanden: ${currency?.gems ?? 0})`, - }); - } - - // Juwelen abziehen & Schleife gutschreiben - await db.query( - "UPDATE account_currency SET gems = gems - ? WHERE account_id = ?", - [LOOP_COST_GEMS, userId] - ); - await db.query( - "UPDATE building_collect_timer SET loops_purchased = loops_purchased + 1 WHERE user_building_id = ?", - [userBuilding.id] - ); - - const newLoops = timer.loops_purchased + 1; - res.json({ - success: true, - loops_purchased: newLoops, - loops_available: MAX_LOOPS - newLoops, - gems_remaining: currency.gems - LOOP_COST_GEMS, + collected: { resource: selected_resource, amount: toAdd }, + next_resource: nextResource, // null wenn Queue leer war + queue_remaining: queue.length, }); } catch (err) { console.error(err);