const express = require("express"); const router = require("express").Router(); const db = require("../database/database"); /* ── Konstanten ─────────────────────────────────── */ const SESSION_HOURS = 5; const MAX_QUEUE_SLOTS = 4; const LOOP_COST_GEMS = 10; const MINE_RESOURCES = ["gold", "iron", "stone", "wood"]; /* ── Auth-Guard ─────────────────────────────────── */ function requireLogin(req, res, next) { if (!req.session?.user) { return res.status(401).json({ error: "Nicht eingeloggt" }); } next(); } /* ───────────────────────────────────────────────── HELPER: Timer sicherstellen ───────────────────────────────────────────────── */ async function ensureTimer(userBuildingId) { const [[existing]] = await db.query( "SELECT last_collected FROM building_collect_timer WHERE user_building_id = ?", [userBuildingId] ); if (!existing) { await db.query( `INSERT INTO building_collect_timer (user_building_id, last_collected, session_started, selected_resource, loop_queue) VALUES (?, NOW(), NOW(), NULL, JSON_ARRAY())`, [userBuildingId] ); } } /* ───────────────────────────────────────────────── HELPER: Produktionsdaten + Timer laden ───────────────────────────────────────────────── */ async function loadMineData(userId, buildingId) { const [rows] = await db.query( `SELECT ub.id AS user_building_id, ub.level, bp.resource, bp.amount, bp.cycle_seconds, bct.last_collected, bct.session_started, bct.selected_resource, bct.loop_queue FROM user_buildings ub JOIN building_production bp ON bp.building_id = ub.building_id AND bp.level = ub.level JOIN building_collect_timer bct ON bct.user_building_id = ub.id WHERE ub.user_id = ? AND ub.building_id = ? AND bp.resource IN ('gold','iron','stone','wood')`, [userId, buildingId] ); return rows; } /* ───────────────────────────────────────────────── HELPER: Maximale Zyklen einer 5h-Session ───────────────────────────────────────────────── */ 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 []; } } /* ───────────────────────────────────────────────── GET /api/mine/:buildingId/status ───────────────────────────────────────────────── */ router.get("/:buildingId/status", requireLogin, async (req, res) => { const userId = req.session.user.id; const buildingId = req.params.buildingId; try { const [[userBuilding]] = await db.query( "SELECT id, level FROM user_buildings WHERE user_id = ? AND building_id = ?", [userId, buildingId] ); if (!userBuilding) { return res.status(404).json({ error: "Gebäude nicht gefunden" }); } await ensureTimer(userBuilding.id); const rows = await loadMineData(userId, buildingId); if (!rows.length) { return res.status(404).json({ error: "Keine Produktionsdaten gefunden" }); } const { cycle_seconds, last_collected, session_started, selected_resource, loop_queue, level } = rows[0]; const queue = parseQueue(loop_queue); const maxCycles = calcMaxCycles(cycle_seconds); const now = Date.now(); /* ── session_started: wie weit ist die 5h-Session? ── */ const sessionElapsed = Math.floor((now - new Date(session_started).getTime()) / 1000); const sessionDuration = SESSION_HOURS * 3600; const isFull = sessionElapsed >= sessionDuration; /* Verbleibende Sessionzeit für den Countdown */ const sessionSecondsLeft = isFull ? 0 : sessionDuration - sessionElapsed; /* ── last_collected: welche Zyklen noch nicht abgeholt? ── */ const collectElapsed = Math.floor((now - new Date(last_collected).getTime()) / 1000); const rawCycles = Math.floor(collectElapsed / cycle_seconds); /* Nicht mehr als max_cycles und nicht über das Session-Ende hinaus */ const maxCyclesLeft = Math.floor(sessionElapsed / cycle_seconds); const cycles = Math.min(rawCycles, maxCycles, maxCyclesLeft); /* Nächster Zyklus: basierend auf last_collected, aber nur wenn Session noch läuft */ const nextCycleIn = isFull ? 0 : cycle_seconds - (collectElapsed % cycle_seconds); const selectedRow = selected_resource ? rows.find(r => r.resource === selected_resource) : null; const availableAmount = selectedRow ? selectedRow.amount * cycles : 0; const production = rows.map(r => ({ resource: r.resource, amount: r.amount })); const [[currency]] = await db.query( "SELECT gems FROM account_currency WHERE account_id = ?", [userId] ); const gems = currency?.gems ?? 0; res.json({ level, cycles, max_cycles: maxCycles, session_hours: SESSION_HOURS, session_seconds_left: sessionSecondsLeft, // ← komplette Session-Restzeit next_cycle_in_seconds: nextCycleIn, // ← bis zum nächsten Zyklus is_full: isFull, ready: cycles > 0 && !!selected_resource, selected_resource, loop_queue: queue, 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, session_started, cycle_seconds, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ───────────────────────────────────────────────── POST /api/mine/:buildingId/select Neue Ressource wählen – setzt BEIDE Zeitstempel ───────────────────────────────────────────────── */ router.post("/:buildingId/select", 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" }); } await ensureTimer(userBuilding.id); const [[prod]] = await db.query( `SELECT bp.resource FROM user_buildings ub JOIN building_production bp ON bp.building_id = ub.building_id AND bp.level = ub.level WHERE ub.id = ? AND bp.resource = ?`, [userBuilding.id, resource] ); if (!prod) { return res.status(400).json({ error: "Ressource nicht verfügbar" }); } /* Beide Zeitstempel auf NOW() – frische Session */ await db.query( `UPDATE building_collect_timer SET selected_resource = ?, last_collected = NOW(), session_started = NOW(), loop_queue = JSON_ARRAY() WHERE user_building_id = ?`, [resource, userBuilding.id] ); res.json({ success: true, selected_resource: resource }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); /* ───────────────────────────────────────────────── POST /api/mine/:buildingId/loop Schleife 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?.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})`, }); } 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. session_started wird NICHT verändert – die 5h laufen unabhängig vom Abholen weiter. ───────────────────────────────────────────────── */ router.post("/:buildingId/collect", 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" }); } await ensureTimer(userBuilding.id); const rows = await loadMineData(userId, buildingId); if (!rows.length) { return res.status(404).json({ error: "Gebäude nicht gefunden" }); } const { user_building_id, cycle_seconds, last_collected, session_started, selected_resource, loop_queue } = rows[0]; if (!selected_resource) { return res.status(400).json({ error: "Keine Ressource ausgewählt" }); } const now = Date.now(); const maxCycles = calcMaxCycles(cycle_seconds); const sessionElapsed = Math.floor((now - new Date(session_started).getTime()) / 1000); const collectElapsed = Math.floor((now - new Date(last_collected).getTime()) / 1000); const maxCyclesLeft = Math.floor(sessionElapsed / cycle_seconds); const rawCycles = Math.floor(collectElapsed / cycle_seconds); const cycles = Math.min(rawCycles, maxCycles, maxCyclesLeft); if (cycles < 1) { const waitSeconds = cycle_seconds - (collectElapsed % cycle_seconds); return res.json({ error: "Noch nichts bereit", ready_in_seconds: waitSeconds, ready_in_display: `${Math.floor(waitSeconds / 60)}m ${waitSeconds % 60}s`, }); } const selectedRow = rows.find(r => r.resource === selected_resource); if (!selectedRow) { 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}\` + ? WHERE account_id = ?`, [toAdd, userId] ); /* last_collected vorsetzen – session_started bleibt unangetastet */ const newLastCollected = new Date( new Date(last_collected).getTime() + cycles * cycle_seconds * 1000 ); /* Prüfen ob Session abgelaufen → Queue-Slot starten */ const isFull = sessionElapsed >= SESSION_HOURS * 3600; const queue = parseQueue(loop_queue); let nextResource = selected_resource; // bleibt aktiv falls Session noch läuft let newQueue = queue; let newSessionStart = null; // wird nur gesetzt wenn Session wechselt if (isFull) { /* Session ist vorbei → nächsten Queue-Slot holen */ nextResource = queue.length > 0 ? queue.shift() : null; newQueue = queue; newSessionStart = new Date(); // neue Session startet jetzt } await db.query( `UPDATE building_collect_timer SET last_collected = ?, session_started = COALESCE(?, session_started), selected_resource = ?, loop_queue = ? WHERE user_building_id = ?`, [ newLastCollected, newSessionStart, // NULL wenn Session noch läuft → COALESCE behält alten Wert nextResource, JSON.stringify(newQueue), user_building_id, ] ); res.json({ success: true, cycles, collected: { resource: selected_resource, amount: toAdd }, session_ended: isFull, next_resource: isFull ? nextResource : null, queue_remaining: newQueue.length, }); } catch (err) { console.error(err); res.status(500).json({ error: "DB Fehler" }); } }); module.exports = router;