417 lines
15 KiB
JavaScript
417 lines
15 KiB
JavaScript
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;
|