This commit is contained in:
cay 2026-04-08 16:48:02 +01:00
parent a2ab6c6297
commit d394657b18
3 changed files with 173 additions and 105 deletions

View File

@ -28,17 +28,14 @@ async function renderMineStatus(buildingId) {
"<div class='mine-panel'>" +
renderHeader(data) +
renderDivider() +
// Ressource wählen (nur wenn noch keine aktive Session)
(!data.selected_resource
? renderResourceSelector(data, buildingId)
: renderActiveSession(data, buildingId)
) +
"</div>";
if (data.selected_resource) {
startCountdown(buildingId, data);
startSessionCountdown(buildingId, data);
}
} catch (err) {
console.error("Mine Fehler:", err);
@ -53,8 +50,8 @@ async function renderMineStatus(buildingId) {
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";
if (data.is_full) cycleText = "Session beendet bitte abholen!";
else if (data.cycles > 0) cycleText = data.cycles + "x Zyklus bereit";
else cycleText = "Läuft...";
}
return (
@ -66,7 +63,7 @@ function renderHeader(data) {
}
/*
Ressourcen-Auswahl (Startscreen ohne aktive Session)
Startscreen (keine aktive Session)
*/
function renderResourceSelector(data, buildingId) {
return (
@ -77,30 +74,32 @@ function renderResourceSelector(data, buildingId) {
}
/*
Aktive Session (mit Timer, Queue, Collect-Button)
Aktive Session
*/
function renderActiveSession(data, buildingId) {
return (
// Aktuelle Produktion
renderCurrentProduction(data) +
renderDivider() +
// Warteschlangen-Bereich
renderQueueSection(data, buildingId) +
renderDivider() +
// Abholen-Button
renderCollectSection(data, buildingId)
);
}
/*
Aktuelle Produktion
Zwei Timer:
1. Session-Countdown wie lange läuft die 5h noch?
2. Zyklus-Countdown wann ist der nächste Zyklus fertig?
*/
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));
const sessionMin = Math.floor(data.session_seconds_left / 60);
const sessionSec = data.session_seconds_left % 60;
const cycleMin = Math.floor(data.next_cycle_in_seconds / 60);
const cycleSec = data.next_cycle_in_seconds % 60;
/* Fortschritt: wieviele Zyklen von max wurden in der Session erreicht */
const progressPct = Math.min(100, Math.round((data.cycles / data.max_cycles) * 100));
return (
"<p class='mine-section-title'>Aktuelle Session " + data.session_hours + "h</p>" +
@ -109,27 +108,38 @@ function renderCurrentProduction(data) {
"<span class='mine-resource-label'>" + resourceLabel(data.selected_resource) + "</span>" +
"<span class='mine-resource-amount'>" + data.available_amount + "</span>" +
"</div>" +
/* Fortschrittsbalken */
"<div class='mine-progress-wrap'>" +
"<div class='mine-progress-bar' style='width:" + progressPct + "%'></div>" +
"</div>" +
/* Session-Timer (läuft immer durch) */
"<div class='mine-timer-row'>" +
"<span class='mine-timer-label'>" +
(data.is_full ? "Zeit abgelaufen!" : "Nächster Zyklus in") +
"</span>" +
"<span class='mine-timer-label'>Session endet in</span>" +
(data.is_full
? "<span class='mine-timer mine-timer-full'>VOLL</span>"
: "<span class='mine-timer' id='mine-countdown' data-seconds='" +
data.next_cycle_in_seconds + "'>" +
minutesLeft + "m " + secondsLeft + "s</span>"
? "<span class='mine-timer mine-timer-full'>FERTIG</span>"
: "<span class='mine-timer' id='mine-session-countdown'" +
" data-seconds='" + data.session_seconds_left + "'>" +
sessionMin + "m " + sessionSec + "s</span>"
) +
"</div>"
"</div>" +
/* Zyklus-Timer (nur wenn Session noch läuft und noch Zyklen kommen) */
(!data.is_full
? "<div class='mine-timer-row mine-timer-row-sub'>" +
"<span class='mine-timer-label'>Nächster Zyklus in</span>" +
"<span class='mine-timer mine-timer-sub' id='mine-cycle-countdown'" +
" data-seconds='" + data.next_cycle_in_seconds + "'>" +
cycleMin + "m " + cycleSec + "s</span>" +
"</div>"
: ""
)
);
}
/*
Warteschlangen-Bereich
zeigt belegte Slots als Ressourcen-Badges
zeigt freie Slots als aufklappbare Auswahl
Warteschlange
*/
function renderQueueSection(data, buildingId) {
const { loop_queue, loop_slots_free, loop_slots_used,
@ -139,7 +149,6 @@ function renderQueueSection(data, buildingId) {
"<p class='mine-section-title'>Warteschlange" +
" <span class='mine-queue-count'>(" + loop_slots_used + "/4)</span></p>";
// Belegte Slots
loop_queue.forEach((res, i) => {
html +=
"<div class='mine-queue-slot mine-queue-slot-filled'>" +
@ -150,11 +159,10 @@ function renderQueueSection(data, buildingId) {
"</div>";
});
// Freie Slots (kaufbar)
if (loop_slots_free > 0) {
const nextPos = loop_slots_used + 1;
html +=
"<div class='mine-queue-add' id='mine-queue-add'>" +
"<div class='mine-queue-add'>" +
"<button class='mine-btn-add-loop" + (can_afford_loop ? "" : " mine-btn-disabled") + "'" +
" id='mine-queue-toggle'" +
" data-building='" + buildingId + "'" +
@ -165,7 +173,6 @@ function renderQueueSection(data, buildingId) {
: "Schleife " + nextPos + " (" + loop_cost_gems + " 💎 nur " + player_gems + " verfügbar)"
) +
"</button>" +
// Resource-Picker (standardmäßig versteckt)
"<div class='mine-loop-picker' id='mine-loop-picker' style='display:none'>" +
"<p class='mine-hint'>Welche Ressource soll in Slot " + nextPos + " abgebaut werden?</p>" +
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) {
"</button>"
);
}).join("");
return "<div class='mine-res-selector'>" + buttons + "</div>";
}
@ -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);
}

View File

@ -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);

View File

@ -8,7 +8,6 @@
href="/images/favicon/dok_favicon_32px.ico"
type="image/x-icon"
/>
<script src="/js/heartbeat.js"></script>
<style>
@font-face {
font-family: "Cinzel Decorative";
@ -96,6 +95,7 @@
<link rel="stylesheet" href="/css/events.css" />
<link rel="stylesheet" href="/css/hud.css" />
<link rel="stylesheet" href="/css/mine.css" />
<script src="/js/heartbeat.js"></script>
</head>
<body>