dok/routes/mine.route.js
2026-04-08 16:48:02 +01:00

417 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;