" +
"
Welche Ressource soll in Slot " + nextPos + " abgebaut werden?
" +
renderResourceGrid(data.production, null, buildingId, "loop") +
@@ -181,8 +188,7 @@ function renderQueueSection(data, buildingId) {
}
/* ─────────────────────────────────────────────────
- Ressourcen-Raster (wiederverwendbar für select + loop)
- mode: "select" | "loop"
+ Ressourcen-Raster
───────────────────────────────────────────────── */
function renderResourceGrid(production, activeResource, buildingId, mode) {
const buttons = production.map(r => {
@@ -199,7 +205,6 @@ function renderResourceGrid(production, activeResource, buildingId, mode) {
""
);
}).join("");
-
return "
" + buttons + "
";
}
@@ -224,22 +229,20 @@ function renderDivider() {
}
/* ─────────────────────────────────────────────────
- Event: Toggle Schleifenpicker
+ Event: Loop-Picker togglen
───────────────────────────────────────────────── */
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)
+ Event: Ressource wählen / Schleife buchen
───────────────────────────────────────────────── */
document.addEventListener("click", async (e) => {
const btn = e.target.closest(".mine-res-btn");
@@ -247,7 +250,7 @@ document.addEventListener("click", async (e) => {
const resource = btn.dataset.resource;
const buildingId = btn.dataset.building;
- const mode = btn.dataset.mode; // "select" | "loop"
+ const mode = btn.dataset.mode;
document.querySelectorAll(".mine-res-btn").forEach(b => (b.disabled = true));
@@ -322,11 +325,13 @@ document.addEventListener("click", async (e) => {
const c = data.collected;
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.";
+
+ if (data.session_ended) {
+ msg += data.next_resource
+ ? "\n\nNächste Session: " + resourceLabel(data.next_resource) + " (5h)"
+ : "\n\nKeine weiteren Schleifen – neue Ressource wählen.";
}
+ /* Session läuft noch → kein Hinweis nötig */
showNotification(msg, "Mine", "⛏️");
await renderMineStatus(buildingId);
@@ -339,17 +344,23 @@ document.addEventListener("click", async (e) => {
});
/* ─────────────────────────────────────────────────
- Countdown-Timer
+ Zwei parallele Countdowns:
+ 1. Session-Countdown (mine-session-countdown)
+ 2. Zyklus-Countdown (mine-cycle-countdown)
───────────────────────────────────────────────── */
-let countdownInterval = null;
+let sessionInterval = null;
+let cycleInterval = null;
+
+function startSessionCountdown(buildingId, data) {
+ if (sessionInterval) clearInterval(sessionInterval);
+ if (cycleInterval) clearInterval(cycleInterval);
-function startCountdown(buildingId, data) {
- if (countdownInterval) clearInterval(countdownInterval);
if (data.is_full) return;
- countdownInterval = setInterval(() => {
- const el = document.getElementById("mine-countdown");
- if (!el) { clearInterval(countdownInterval); return; }
+ /* Session-Timer */
+ sessionInterval = setInterval(() => {
+ const el = document.getElementById("mine-session-countdown");
+ if (!el) { clearInterval(sessionInterval); return; }
let secs = parseInt(el.dataset.seconds, 10) - 1;
if (secs < 0) secs = 0;
@@ -357,8 +368,25 @@ function startCountdown(buildingId, data) {
el.textContent = Math.floor(secs / 60) + "m " + (secs % 60) + "s";
if (secs === 0) {
- clearInterval(countdownInterval);
- renderMineStatus(buildingId);
+ clearInterval(sessionInterval);
+ clearInterval(cycleInterval);
+ renderMineStatus(buildingId); // Session abgelaufen → neu rendern
+ }
+ }, 1000);
+
+ /* Zyklus-Timer */
+ cycleInterval = setInterval(() => {
+ const el = document.getElementById("mine-cycle-countdown");
+ if (!el) { clearInterval(cycleInterval); return; }
+
+ let secs = parseInt(el.dataset.seconds, 10) - 1;
+ if (secs < 0) secs = 0;
+ el.dataset.seconds = secs;
+ el.textContent = Math.floor(secs / 60) + "m " + (secs % 60) + "s";
+
+ if (secs === 0) {
+ clearInterval(cycleInterval);
+ renderMineStatus(buildingId); // Neuer Zyklus → Menge aktualisieren
}
}, 1000);
}
diff --git a/routes/mine.route.js b/routes/mine.route.js
index 2032811..17a8acb 100644
--- a/routes/mine.route.js
+++ b/routes/mine.route.js
@@ -3,10 +3,10 @@ const router = require("express").Router();
const db = require("../database/database");
/* ── Konstanten ─────────────────────────────────── */
-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"];
+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) {
@@ -27,8 +27,8 @@ async function ensureTimer(userBuildingId) {
if (!existing) {
await db.query(
`INSERT INTO building_collect_timer
- (user_building_id, last_collected, selected_resource, loop_queue)
- VALUES (?, NOW(), NULL, JSON_ARRAY())`,
+ (user_building_id, last_collected, session_started, selected_resource, loop_queue)
+ VALUES (?, NOW(), NOW(), NULL, JSON_ARRAY())`,
[userBuildingId]
);
}
@@ -46,6 +46,7 @@ async function loadMineData(userId, buildingId) {
bp.amount,
bp.cycle_seconds,
bct.last_collected,
+ bct.session_started,
bct.selected_resource,
bct.loop_queue
FROM user_buildings ub
@@ -77,9 +78,7 @@ function parseQueue(raw) {
try {
const q = typeof raw === "string" ? JSON.parse(raw) : raw;
return Array.isArray(q) ? q : [];
- } catch {
- return [];
- }
+ } catch { return []; }
}
/* ─────────────────────────────────────────────────
@@ -105,18 +104,34 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
}
const {
- cycle_seconds, last_collected,
+ 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();
- 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);
+ /* ── 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)
@@ -134,21 +149,23 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
res.json({
level,
cycles,
- max_cycles: maxCycles,
- session_hours: SESSION_HOURS,
- is_full: isFull,
- ready: cycles > 0 && !!selected_resource,
+ 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, // 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,
+ 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,
- next_cycle_in_seconds: nextIn,
+ session_started,
cycle_seconds,
});
} catch (err) {
@@ -159,7 +176,7 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
/* ─────────────────────────────────────────────────
POST /api/mine/:buildingId/select
- Erste Ressource wählen – startet Session neu
+ Neue Ressource wählen – setzt BEIDE Zeitstempel
───────────────────────────────────────────────── */
router.post("/:buildingId/select", requireLogin, async (req, res) => {
const userId = req.session.user.id;
@@ -193,10 +210,13 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => {
return res.status(400).json({ error: "Ressource nicht verfügbar" });
}
- // Session neu starten, Warteschlange leeren
+ /* Beide Zeitstempel auf NOW() – frische Session */
await db.query(
`UPDATE building_collect_timer
- SET selected_resource = ?, last_collected = NOW(), loop_queue = JSON_ARRAY()
+ SET selected_resource = ?,
+ last_collected = NOW(),
+ session_started = NOW(),
+ loop_queue = JSON_ARRAY()
WHERE user_building_id = ?`,
[resource, userBuilding.id]
);
@@ -210,7 +230,7 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => {
/* ─────────────────────────────────────────────────
POST /api/mine/:buildingId/loop
- Schleife mit eigener Ressourcenwahl zur Queue hinzufügen
+ Schleife zur Queue hinzufügen
───────────────────────────────────────────────── */
router.post("/:buildingId/loop", requireLogin, async (req, res) => {
const userId = req.session.user.id;
@@ -234,10 +254,7 @@ router.post("/:buildingId/loop", requireLogin, async (req, res) => {
"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) {
+ if (!timer?.selected_resource) {
return res.status(400).json({ error: "Erst eine Ressource für die aktuelle Session wählen" });
}
@@ -256,7 +273,6 @@ router.post("/:buildingId/loop", requireLogin, async (req, res) => {
});
}
- // 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]
@@ -284,7 +300,9 @@ router.post("/:buildingId/loop", requireLogin, async (req, res) => {
/* ─────────────────────────────────────────────────
POST /api/mine/:buildingId/collect
- Ressourcen gutschreiben + nächsten Queue-Slot starten
+ 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;
@@ -307,20 +325,24 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => {
const {
user_building_id, cycle_seconds, last_collected,
- selected_resource, loop_queue
+ session_started, selected_resource, loop_queue
} = rows[0];
if (!selected_resource) {
return res.status(400).json({ error: "Keine Ressource ausgewählt" });
}
- 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 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 - elapsed;
+ const waitSeconds = cycle_seconds - (collectElapsed % cycle_seconds);
return res.json({
error: "Noch nichts bereit",
ready_in_seconds: waitSeconds,
@@ -333,7 +355,7 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => {
return res.status(400).json({ error: "Ressource nicht gefunden" });
}
- // Ressource gutschreiben
+ /* Ressource gutschreiben */
const toAdd = selectedRow.amount * cycles;
await db.query(
`UPDATE account_currency
@@ -342,30 +364,48 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => {
[toAdd, userId]
);
- // 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)
+ /* 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, nextResource, JSON.stringify(queue), 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 },
- next_resource: nextResource, // null wenn Queue leer war
- queue_remaining: queue.length,
+ session_ended: isFull,
+ next_resource: isFull ? nextResource : null,
+ queue_remaining: newQueue.length,
});
} catch (err) {
console.error(err);
diff --git a/views/launcher.ejs b/views/launcher.ejs
index 8409552..ae20a61 100644
--- a/views/launcher.ejs
+++ b/views/launcher.ejs
@@ -8,7 +8,6 @@
href="/images/favicon/dok_favicon_32px.ico"
type="image/x-icon"
/>
-