This commit is contained in:
cay 2026-04-08 14:15:17 +01:00
parent efe45c3591
commit c3addb7ba1
3 changed files with 422 additions and 354 deletions

View File

@ -1,7 +1,6 @@
/* ================================================
mine.css Mine Gebäude UI
Passt zum bestehenden Design aus building.css
und dem qm-popup / game-notification System
================================================ */
/* ── Panel-Wrapper ─────────────────────────────── */
@ -12,7 +11,7 @@
gap: 0;
}
/* ── Header-Zeile: Level + Zyklusinfo ──────────── */
/* ── Header ────────────────────────────────────── */
.mine-header-row {
display: flex;
align-items: center;
@ -53,7 +52,27 @@
margin: 0 0 8px 0;
}
/* ── Ressourcen-Auswahl ────────────────────────── */
.mine-queue-count {
color: #c8952a;
font-weight: normal;
text-transform: none;
letter-spacing: 0;
}
/* ── Hinweistext ───────────────────────────────── */
.mine-hint {
font-size: 11px;
color: #806040;
margin: 0 0 8px 0;
line-height: 1.4;
}
.mine-hint-center {
text-align: center;
margin-top: 6px;
}
/* ── Ressourcen-Auswahl Raster ─────────────────── */
.mine-res-selector {
display: grid;
grid-template-columns: 1fr 1fr;
@ -118,17 +137,11 @@
}
/* ── Aktuelle Produktion ───────────────────────── */
.mine-resources {
display: flex;
flex-direction: column;
gap: 6px;
}
.mine-resource-row {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.03);
background: rgba(255,255,255,0.03);
border: 1px solid rgba(122, 90, 26, 0.3);
border-radius: 6px;
padding: 7px 12px;
@ -149,7 +162,6 @@
justify-content: center;
}
/* Ressource Icons einheitliche Größe */
.mine-resource-icon-gold,
.mine-resource-icon-iron,
.mine-resource-icon-wood,
@ -235,96 +247,113 @@
animation: minePulse 1s ease-in-out infinite;
}
/* ── Schleifen-Bereich ─────────────────────────── */
.mine-loop-row {
/* ── Warteschlangen-Slots ──────────────────────── */
.mine-queue-slot {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.mine-loop-dots {
display: flex;
gap: 6px;
align-items: center;
}
.mine-loop-dot {
width: 14px;
height: 14px;
border-radius: 50%;
background: rgba(255,255,255,0.08);
border: 1px solid rgba(122, 90, 26, 0.5);
transition: background 0.3s, border-color 0.3s;
}
.mine-loop-dot-active {
background: linear-gradient(135deg, #a030f0, #6010c0);
border-color: #c060ff;
box-shadow: 0 0 6px rgba(160, 48, 240, 0.6);
}
.mine-loop-info {
gap: 8px;
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 5px;
font-size: 12px;
color: #a07830;
}
.mine-loop-maxed {
color: #f0c84a;
.mine-queue-slot-filled {
background: rgba(160, 48, 240, 0.08);
border: 1px solid rgba(160, 48, 240, 0.35);
}
.mine-queue-pos {
font-size: 11px;
color: #806080;
min-width: 16px;
}
.mine-queue-icon {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
}
.mine-queue-icon img {
width: 22px;
height: 22px;
object-fit: contain;
filter: drop-shadow(0 1px 2px rgba(0,0,0,0.6));
display: block;
}
.mine-queue-label {
flex: 1;
color: #d0a0ff;
font-weight: bold;
}
.mine-btn-loop {
.mine-queue-dur {
font-size: 11px;
color: #806080;
}
/* ── Schleife hinzufügen ───────────────────────── */
.mine-queue-add {
margin-bottom: 2px;
}
.mine-btn-add-loop {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
background: linear-gradient(180deg, #3a1060 0%, #200840 100%);
color: #d090ff;
border: 1px solid #8040c0;
background: linear-gradient(180deg, #2a1040 0%, #180828 100%);
color: #c080ff;
border: 1px dashed #6030a0;
border-radius: 6px;
padding: 8px 16px;
padding: 8px 14px;
font-size: 12px;
font-family: sans-serif;
font-weight: bold;
cursor: pointer;
letter-spacing: 0.03em;
transition: filter 0.15s, transform 0.1s;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
letter-spacing: 0.02em;
transition: border-color 0.2s, background 0.2s, transform 0.1s;
}
.mine-btn-loop:hover:not(:disabled) {
filter: brightness(1.25);
.mine-btn-add-loop:hover:not(:disabled) {
border-color: #a060e0;
background: linear-gradient(180deg, #3a1860 0%, #280a40 100%);
transform: translateY(-1px);
}
.mine-btn-loop:active:not(:disabled) {
transform: translateY(0);
filter: brightness(0.9);
.mine-btn-add-loop-open {
border-style: solid;
border-color: #a060e0;
background: linear-gradient(180deg, #3a1860 0%, #280a40 100%);
}
.mine-btn-loop.mine-btn-disabled,
.mine-btn-loop:disabled {
background: linear-gradient(180deg, #1a1020 0%, #100810 100%);
border-color: #3a2050;
color: #604080;
.mine-btn-add-loop.mine-btn-disabled,
.mine-btn-add-loop:disabled {
background: linear-gradient(180deg, #1a1020 0%, #100818 100%);
border-color: #301850;
color: #503060;
cursor: not-allowed;
box-shadow: none;
transform: none;
filter: none;
}
/* ── Picker im Loop-Bereich ────────────────────── */
.mine-loop-picker {
margin-top: 8px;
padding: 10px;
background: rgba(100, 30, 160, 0.07);
border: 1px solid rgba(100, 30, 160, 0.25);
border-radius: 6px;
}
.mine-loop-gem-icon {
font-size: 14px;
}
.mine-loop-no-gems {
font-size: 10px;
color: #804060;
font-weight: normal;
}
/* ── Aktions-Bereich ───────────────────────────── */
.mine-actions {
margin-top: 4px;

View File

@ -28,12 +28,13 @@ async function renderMineStatus(buildingId) {
"<div class='mine-panel'>" +
renderHeader(data) +
renderDivider() +
renderResourceSelector(data, buildingId) +
(data.selected_resource ? renderProductionSection(data) : "") +
(data.selected_resource ? renderDivider() : "") +
(data.selected_resource ? renderLoopSection(data, buildingId) : "") +
(data.selected_resource ? renderDivider() : "") +
(data.selected_resource ? renderCollectSection(data, buildingId) : "") +
// Ressource wählen (nur wenn noch keine aktive Session)
(!data.selected_resource
? renderResourceSelector(data, buildingId)
: renderActiveSession(data, buildingId)
) +
"</div>";
if (data.selected_resource) {
@ -47,18 +48,14 @@ async function renderMineStatus(buildingId) {
}
/*
Abschnitt: Header (Level + Zyklusinfo)
Header
*/
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";
} else {
cycleText = "Läuft...";
}
if (data.is_full) cycleText = "Voll bitte abholen!";
else if (data.cycles > 0) cycleText = data.cycles + "x Zyklus abgeschlossen";
else cycleText = "Läuft...";
}
return (
"<div class='mine-header-row'>" +
@ -69,18 +66,133 @@ function renderHeader(data) {
}
/*
Abschnitt: Ressourcen-Auswahl
Ressourcen-Auswahl (Startscreen ohne aktive Session)
*/
function renderResourceSelector(data, buildingId) {
const resources = data.production; // [{resource, amount}]
return (
"<p class='mine-section-title'>Ressource wählen</p>" +
"<p class='mine-hint'>Wähle eine Ressource für die nächsten " + data.session_hours + "h.</p>" +
renderResourceGrid(data.production, null, buildingId, "select")
);
}
const buttons = resources.map(r => {
const isSelected = data.selected_resource === r.resource;
/*
Aktive Session (mit Timer, Queue, Collect-Button)
*/
function renderActiveSession(data, buildingId) {
return (
// Aktuelle Produktion
renderCurrentProduction(data) +
renderDivider() +
// Warteschlangen-Bereich
renderQueueSection(data, buildingId) +
renderDivider() +
// Abholen-Button
renderCollectSection(data, buildingId)
);
}
/*
Aktuelle Produktion
*/
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));
return (
"<p class='mine-section-title'>Aktuelle Session " + data.session_hours + "h</p>" +
"<div class='mine-resource-row" + (data.ready ? " mine-resource-ready" : "") + "'>" +
"<span class='mine-resource-icon'>" + resourceIcon(data.selected_resource) + "</span>" +
"<span class='mine-resource-label'>" + resourceLabel(data.selected_resource) + "</span>" +
"<span class='mine-resource-amount'>" + data.available_amount + "</span>" +
"</div>" +
"<div class='mine-progress-wrap'>" +
"<div class='mine-progress-bar' style='width:" + progressPct + "%'></div>" +
"</div>" +
"<div class='mine-timer-row'>" +
"<span class='mine-timer-label'>" +
(data.is_full ? "Zeit abgelaufen!" : "Nächster Zyklus 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>"
) +
"</div>"
);
}
/*
Warteschlangen-Bereich
zeigt belegte Slots als Ressourcen-Badges
zeigt freie Slots als aufklappbare Auswahl
*/
function renderQueueSection(data, buildingId) {
const { loop_queue, loop_slots_free, loop_slots_used,
loop_cost_gems, can_afford_loop, player_gems } = data;
let html =
"<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'>" +
"<span class='mine-queue-pos'>" + (i + 1) + ".</span>" +
"<span class='mine-queue-icon'>" + resourceIcon(res) + "</span>" +
"<span class='mine-queue-label'>" + resourceLabel(res) + "</span>" +
"<span class='mine-queue-dur'>" + data.session_hours + "h</span>" +
"</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'>" +
"<button class='mine-btn-add-loop" + (can_afford_loop ? "" : " mine-btn-disabled") + "'" +
" id='mine-queue-toggle'" +
" data-building='" + buildingId + "'" +
(can_afford_loop ? "" : " disabled") + ">" +
"<span class='mine-loop-gem-icon'>💎</span>" +
(can_afford_loop
? "Schleife " + nextPos + " hinzufügen (" + loop_cost_gems + " Juwelen)"
: "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") +
"</div>" +
"</div>";
}
if (loop_slots_free === 0) {
html += "<p class='mine-hint mine-hint-center'>Warteschlange voll 4 Schleifen aktiv.</p>";
}
return html;
}
/*
Ressourcen-Raster (wiederverwendbar für select + loop)
mode: "select" | "loop"
*/
function renderResourceGrid(production, activeResource, buildingId, mode) {
const buttons = production.map(r => {
const isActive = r.resource === activeResource;
return (
"<button class='mine-res-btn" +
(isSelected ? " mine-res-btn-active" : "") +
"' data-resource='" + r.resource +
"' data-building='" + buildingId + "'>" +
"<button class='mine-res-btn" + (isActive ? " mine-res-btn-active" : "") + "'" +
" data-resource='" + r.resource + "'" +
" data-building='" + buildingId + "'" +
" data-mode='" + mode + "'" +
(isActive ? " disabled" : "") + ">" +
"<span class='mine-res-btn-icon'>" + resourceIcon(r.resource) + "</span>" +
"<span class='mine-res-btn-name'>" + resourceLabel(r.resource) + "</span>" +
"<span class='mine-res-btn-amount'>+" + r.amount + "/Zyklus</span>" +
@ -88,94 +200,11 @@ function renderResourceSelector(data, buildingId) {
);
}).join("");
return (
"<p class='mine-section-title'>Ressource wählen</p>" +
"<div class='mine-res-selector'>" +
buttons +
"</div>"
);
return "<div class='mine-res-selector'>" + buttons + "</div>";
}
/*
Abschnitt: Aktuelle Produktion
*/
function renderProductionSection(data) {
const minutesLeft = Math.floor(data.next_cycle_in_seconds / 60);
const secondsLeft = data.next_cycle_in_seconds % 60;
const maxHoursDisplay = data.max_hours + "h";
const progressPercent = Math.min(
100,
Math.round((data.cycles / data.max_cycles) * 100)
);
return (
renderDivider() +
"<p class='mine-section-title'>Aktuelle Produktion</p>" +
"<div class='mine-resource-row" + (data.ready ? " mine-resource-ready" : "") + "'>" +
"<span class='mine-resource-icon'>" + resourceIcon(data.selected_resource) + "</span>" +
"<span class='mine-resource-label'>" + resourceLabel(data.selected_resource) + "</span>" +
"<span class='mine-resource-amount'>" + data.available_amount + "</span>" +
"</div>" +
"<div class='mine-progress-wrap'>" +
"<div class='mine-progress-bar' style='width:" + progressPercent + "%'></div>" +
"</div>" +
"<div class='mine-timer-row'>" +
"<span class='mine-timer-label'>" +
(data.is_full
? "Maximale Zeit erreicht (" + maxHoursDisplay + ")"
: "Nächster Zyklus 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>") +
"</div>"
);
}
/*
Abschnitt: Schleifen (Loops)
*/
function renderLoopSection(data, buildingId) {
const loopDots = [];
for (let i = 0; i < 4; i++) {
loopDots.push(
"<span class='mine-loop-dot" +
(i < data.loops_purchased ? " mine-loop-dot-active" : "") +
"'></span>"
);
}
const canBuy = data.loops_available > 0 && data.can_afford_loop;
const maxReached = data.loops_available === 0;
return (
"<p class='mine-section-title'>Schleifen</p>" +
"<div class='mine-loop-row'>" +
"<div class='mine-loop-dots'>" + loopDots.join("") + "</div>" +
"<span class='mine-loop-info'>" +
data.max_hours + "h max" +
(maxReached ? " <span class='mine-loop-maxed'>(Max)</span>" : "") +
"</span>" +
"</div>" +
(!maxReached
? "<button class='mine-btn-loop" + (canBuy ? "" : " mine-btn-disabled") + "'" +
" id='mine-loop-btn'" +
" data-building='" + buildingId + "'" +
(canBuy ? "" : " disabled") + ">" +
"<span class='mine-loop-gem-icon'>💎</span>" +
"10 Juwelen → +5h" +
(data.can_afford_loop ? "" : " <span class='mine-loop-no-gems'>(" + data.player_gems + " verfügbar)</span>") +
"</button>"
: ""
)
);
}
/*
Abschnitt: Abholen-Button
Collect-Button
*/
function renderCollectSection(data, buildingId) {
return (
@ -195,20 +224,39 @@ function renderDivider() {
}
/*
Event-Delegation: Ressource wählen
Event: Toggle Schleifenpicker
*/
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)
*/
document.addEventListener("click", async (e) => {
const btn = e.target.closest(".mine-res-btn");
if (!btn || btn.classList.contains("mine-res-btn-active")) return;
if (!btn || btn.disabled || btn.classList.contains("mine-res-btn-active")) return;
const resource = btn.dataset.resource;
const buildingId = btn.dataset.building;
const mode = btn.dataset.mode; // "select" | "loop"
// Alle Buttons kurz deaktivieren
document.querySelectorAll(".mine-res-btn").forEach(b => b.disabled = true);
document.querySelectorAll(".mine-res-btn").forEach(b => (b.disabled = true));
const endpoint = mode === "loop"
? "/api/mine/" + buildingId + "/loop"
: "/api/mine/" + buildingId + "/select";
try {
const res = await fetch("/api/mine/" + buildingId + "/select", {
const res = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ resource }),
@ -222,64 +270,37 @@ document.addEventListener("click", async (e) => {
return;
}
showNotification(
resourceLabel(resource) + " ausgewählt.\nTimer wurde zurückgesetzt.",
"Mine", "⛏️"
);
await renderMineStatus(buildingId);
} catch (err) {
console.error("Ressource wählen Fehler:", err);
await renderMineStatus(buildingId);
}
});
/*
Event-Delegation: Schleife kaufen
*/
document.addEventListener("click", async (e) => {
const btn = e.target.closest("#mine-loop-btn");
if (!btn || btn.disabled) return;
const buildingId = btn.dataset.building;
btn.disabled = true;
btn.textContent = "Kaufe...";
try {
const res = await fetch("/api/mine/" + buildingId + "/loop", {
method: "POST",
});
if (!res.ok) throw new Error("API Fehler");
const data = await res.json();
if (data.error) {
showNotification(data.error, "Mine", "⛏️");
await renderMineStatus(buildingId);
return;
if (mode === "loop") {
showNotification(
"Schleife hinzugefügt!\n" + resourceLabel(resource) + " 5h\n" +
"Noch " + data.loop_slots_free + " Slot(s) frei.",
"Mine", "💎"
);
} else {
showNotification(
resourceLabel(resource) + " ausgewählt.\n5h Session startet jetzt.",
"Mine", "⛏️"
);
}
showNotification(
"Schleife aktiviert! +" + 5 + "h Abbauzeit.\n" +
"Noch " + data.loops_available + " Schleife(n) verfügbar.",
"Mine", "💎"
);
await renderMineStatus(buildingId);
await refreshHud();
if (mode === "loop") await refreshHud();
} catch (err) {
console.error("Schleife kaufen Fehler:", err);
console.error("Mine Fehler:", err);
await renderMineStatus(buildingId);
}
});
/*
Event-Delegation: Ressource abholen
Event: Abholen
*/
document.addEventListener("click", async (e) => {
const btn = e.target.closest("#mine-collect-btn");
if (!btn || btn.disabled) return;
const buildingId = btn.dataset.building;
btn.disabled = true;
btn.textContent = "Wird abgeholt...";
btn.disabled = true;
btn.textContent = "Wird abgeholt...";
try {
const res = await fetch("/api/mine/" + buildingId + "/collect", {
@ -300,10 +321,14 @@ document.addEventListener("click", async (e) => {
}
const c = data.collected;
showNotification(
"Abgeholt!\n" + resourceLabel(c.resource) + ": +" + c.amount,
"Mine", "⛏️"
);
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.";
}
showNotification(msg, "Mine", "⛏️");
await renderMineStatus(buildingId);
await refreshHud();
} catch (err) {
@ -359,11 +384,6 @@ function resourceIcon(resource) {
}
function resourceLabel(resource) {
const map = {
gold: "Gold",
iron: "Eisen",
stone: "Stein",
wood: "Holz",
};
const map = { gold: "Gold", iron: "Eisen", stone: "Stein", wood: "Holz" };
return map[resource] || resource;
}

View File

@ -1,12 +1,11 @@
const express = require("express");
const router = require("express").Router();
const db = require("../database/database");
const router = require("express").Router();
const db = require("../database/database");
/* ── Konstanten ─────────────────────────────────── */
const MAX_BASE_HOURS = 5; // Basis-Stunden pro Session
const MAX_LOOPS = 4; // Maximale kaufbare Schleifen
const LOOP_COST_GEMS = 10; // Juwelen pro Schleife
const LOOP_HOURS = 5; // Zusatzstunden pro Schleife
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"];
/* ── Auth-Guard ─────────────────────────────────── */
@ -19,8 +18,6 @@ function requireLogin(req, res, next) {
/*
HELPER: Timer sicherstellen
Legt beim allerersten Aufruf einen Eintrag an.
Wird durch select() und collect() zurückgesetzt.
*/
async function ensureTimer(userBuildingId) {
const [[existing]] = await db.query(
@ -30,16 +27,15 @@ async function ensureTimer(userBuildingId) {
if (!existing) {
await db.query(
`INSERT INTO building_collect_timer
(user_building_id, last_collected, selected_resource, loops_purchased)
VALUES (?, NOW(), NULL, 0)`,
(user_building_id, last_collected, selected_resource, loop_queue)
VALUES (?, NOW(), NULL, JSON_ARRAY())`,
[userBuildingId]
);
}
}
/*
HELPER: Produktionsdaten laden
Filtert auf die vier abbaubaren Ressourcen.
HELPER: Produktionsdaten + Timer laden
*/
async function loadMineData(userId, buildingId) {
const [rows] = await db.query(
@ -51,7 +47,7 @@ async function loadMineData(userId, buildingId) {
bp.cycle_seconds,
bct.last_collected,
bct.selected_resource,
bct.loops_purchased
bct.loop_queue
FROM user_buildings ub
JOIN building_production bp
ON bp.building_id = ub.building_id
@ -67,12 +63,23 @@ async function loadMineData(userId, buildingId) {
}
/*
HELPER: Maximale Zyklen für diese Session
Basis 5h + je 5h pro gekaufter Schleife
HELPER: Maximale Zyklen einer 5h-Session
*/
function calcMaxCycles(loopsPurchased, cycleSeconds) {
const maxHours = MAX_BASE_HOURS + loopsPurchased * LOOP_HOURS;
return Math.floor((maxHours * 3600) / cycleSeconds);
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 [];
}
}
/*
@ -99,29 +106,25 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
const {
cycle_seconds, last_collected,
selected_resource, loops_purchased, level
selected_resource, loop_queue, level
} = rows[0];
const maxCycles = calcMaxCycles(loops_purchased, cycle_seconds);
const maxHours = MAX_BASE_HOURS + loops_purchased * LOOP_HOURS;
const queue = parseQueue(loop_queue);
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 isFull = rawCycles >= maxCycles;
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);
const nextIn = isFull ? 0 : cycle_seconds - (elapsed % cycle_seconds);
// Menge der gewählten Ressource
const selectedRow = selected_resource
const selectedRow = selected_resource
? rows.find(r => r.resource === selected_resource)
: null;
const availableAmount = selectedRow ? selectedRow.amount * cycles : 0;
// Produktionsübersicht aller vier Ressourcen
const production = rows.map(r => ({ resource: r.resource, amount: r.amount }));
// Juwelen des Spielers für Loop-Anzeige
const [[currency]] = await db.query(
"SELECT gems FROM account_currency WHERE account_id = ?",
[userId]
@ -131,17 +134,18 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
res.json({
level,
cycles,
max_cycles: maxCycles,
max_hours: maxHours,
is_full: isFull,
ready: cycles > 0 && !!selected_resource,
max_cycles: maxCycles,
session_hours: SESSION_HOURS,
is_full: isFull,
ready: cycles > 0 && !!selected_resource,
selected_resource,
loops_purchased,
loops_available: MAX_LOOPS - loops_purchased,
loop_cost_gems: LOOP_COST_GEMS,
can_afford_loop: gems >= LOOP_COST_GEMS,
player_gems: gems,
available_amount: availableAmount,
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,
production,
last_collected,
next_cycle_in_seconds: nextIn,
@ -155,7 +159,7 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
/*
POST /api/mine/:buildingId/select
Ressource wählen setzt Timer und Schleifen zurück
Erste Ressource wählen startet Session neu
*/
router.post("/:buildingId/select", requireLogin, async (req, res) => {
const userId = req.session.user.id;
@ -177,7 +181,6 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => {
await ensureTimer(userBuilding.id);
// Prüfen ob Ressource in building_production vorhanden
const [[prod]] = await db.query(
`SELECT bp.resource
FROM user_buildings ub
@ -187,13 +190,13 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => {
[userBuilding.id, resource]
);
if (!prod) {
return res.status(400).json({ error: "Ressource für dieses Gebäude nicht verfügbar" });
return res.status(400).json({ error: "Ressource nicht verfügbar" });
}
// Timer zurücksetzen, Ressource setzen, Schleifen zurücksetzen
// Session neu starten, Warteschlange leeren
await db.query(
`UPDATE building_collect_timer
SET selected_resource = ?, last_collected = NOW(), loops_purchased = 0
SET selected_resource = ?, last_collected = NOW(), loop_queue = JSON_ARRAY()
WHERE user_building_id = ?`,
[resource, userBuilding.id]
);
@ -205,9 +208,83 @@ router.post("/:buildingId/select", requireLogin, async (req, res) => {
}
});
/*
POST /api/mine/:buildingId/loop
Schleife mit eigener Ressourcenwahl 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) {
return res.status(404).json({ error: "Timer nicht gefunden" });
}
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})`,
});
}
// 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]
);
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 + Timer vorsetzen
Ressourcen gutschreiben + nächsten Queue-Slot starten
*/
router.post("/:buildingId/collect", requireLogin, async (req, res) => {
const userId = req.session.user.id;
@ -230,14 +307,14 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => {
const {
user_building_id, cycle_seconds, last_collected,
selected_resource, loops_purchased
selected_resource, loop_queue
} = rows[0];
if (!selected_resource) {
return res.status(400).json({ error: "Keine Ressource ausgewählt" });
}
const maxCycles = calcMaxCycles(loops_purchased, cycle_seconds);
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);
@ -256,8 +333,8 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => {
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}\` + ?
@ -265,88 +342,30 @@ router.post("/:buildingId/collect", requireLogin, async (req, res) => {
[toAdd, userId]
);
// Timer exakt vorsetzen Restsekunden bleiben erhalten
// 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)
const newLastCollected = new Date(
new Date(last_collected).getTime() + cycles * cycle_seconds * 1000
);
// Schleifen nach dem Abholen zurücksetzen
await db.query(
`UPDATE building_collect_timer
SET last_collected = ?, loops_purchased = 0
SET last_collected = ?,
selected_resource = ?,
loop_queue = ?
WHERE user_building_id = ?`,
[newLastCollected, user_building_id]
[newLastCollected, nextResource, JSON.stringify(queue), user_building_id]
);
res.json({
success: true,
success: true,
cycles,
collected: { resource: selected_resource, amount: toAdd },
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "DB Fehler" });
}
});
/*
POST /api/mine/:buildingId/loop
Schleife für 10 Juwelen kaufen (max. 4 pro Session)
*/
router.post("/:buildingId/loop", 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" });
}
const [[timer]] = await db.query(
"SELECT loops_purchased, 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) {
return res.status(400).json({ error: "Erst eine Ressource auswählen" });
}
if (timer.loops_purchased >= MAX_LOOPS) {
return res.status(400).json({ error: "Maximale Anzahl Schleifen bereits erreicht" });
}
// Juwelen prüfen
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})`,
});
}
// Juwelen abziehen & Schleife gutschreiben
await db.query(
"UPDATE account_currency SET gems = gems - ? WHERE account_id = ?",
[LOOP_COST_GEMS, userId]
);
await db.query(
"UPDATE building_collect_timer SET loops_purchased = loops_purchased + 1 WHERE user_building_id = ?",
[userBuilding.id]
);
const newLoops = timer.loops_purchased + 1;
res.json({
success: true,
loops_purchased: newLoops,
loops_available: MAX_LOOPS - newLoops,
gems_remaining: currency.gems - LOOP_COST_GEMS,
collected: { resource: selected_resource, amount: toAdd },
next_resource: nextResource, // null wenn Queue leer war
queue_remaining: queue.length,
});
} catch (err) {
console.error(err);