This commit is contained in:
cay 2026-04-08 14:07:53 +01:00
parent c61cddddb6
commit efe45c3591
3 changed files with 734 additions and 276 deletions

View File

@ -53,7 +53,71 @@
margin: 0 0 8px 0;
}
/* ── Ressourcen-Liste ──────────────────────────── */
/* ── Ressourcen-Auswahl ────────────────────────── */
.mine-res-selector {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 4px;
}
.mine-res-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: linear-gradient(180deg, #2a1e0a 0%, #1a1206 100%);
border: 1px solid rgba(122, 90, 26, 0.5);
border-radius: 8px;
padding: 10px 8px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s, transform 0.1s;
color: #c8a860;
}
.mine-res-btn:hover:not(:disabled):not(.mine-res-btn-active) {
border-color: #c8952a;
background: linear-gradient(180deg, #3a2a0e 0%, #2a1e08 100%);
transform: translateY(-1px);
}
.mine-res-btn-active {
background: linear-gradient(180deg, #4a3010 0%, #3a2008 100%);
border-color: #f0c84a;
box-shadow: 0 0 8px rgba(240, 200, 74, 0.25);
cursor: default;
}
.mine-res-btn-icon {
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
}
.mine-res-btn-icon img {
width: 28px;
height: 28px;
object-fit: contain;
filter: drop-shadow(0 1px 3px rgba(0,0,0,0.7));
display: block;
}
.mine-res-btn-name {
font-size: 12px;
font-weight: bold;
color: #f0c84a;
}
.mine-res-btn-amount {
font-size: 10px;
color: #a07830;
}
/* ── Aktuelle Produktion ───────────────────────── */
.mine-resources {
display: flex;
flex-direction: column;
@ -68,9 +132,7 @@
border: 1px solid rgba(122, 90, 26, 0.3);
border-radius: 6px;
padding: 7px 12px;
transition:
border-color 0.2s,
background 0.2s;
transition: border-color 0.2s, background 0.2s;
}
.mine-resource-row.mine-resource-ready {
@ -87,31 +149,27 @@
justify-content: center;
}
/* Ressource Icons (gem, silver, wood, stone, gold, iron) einheitliche Größe */
.mine-resource-icon-gem,
.mine-resource-icon-silver,
.mine-resource-icon-wood,
.mine-resource-icon-stone,
/* Ressource Icons einheitliche Größe */
.mine-resource-icon-gold,
.mine-resource-icon-iron {
width: 100px;
height: 100px;
.mine-resource-icon-iron,
.mine-resource-icon-wood,
.mine-resource-icon-stone {
width: 28px;
height: 28px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.mine-resource-icon-gem img,
.mine-resource-icon-silver img,
.mine-resource-icon-wood img,
.mine-resource-icon-stone img,
.mine-resource-icon-gold img,
.mine-resource-icon-iron img {
width: 100px;
height: 100px;
.mine-resource-icon-iron img,
.mine-resource-icon-wood img,
.mine-resource-icon-stone img {
width: 28px;
height: 28px;
object-fit: contain;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.7));
filter: drop-shadow(0 1px 3px rgba(0,0,0,0.7));
display: block;
}
@ -120,7 +178,6 @@
font-size: 13px;
font-weight: bold;
color: #ffffff;
text-shadow: none;
}
.mine-resource-amount {
@ -129,7 +186,23 @@
color: #f0c84a;
min-width: 32px;
text-align: right;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
text-shadow: 0 1px 3px rgba(0,0,0,0.6);
}
/* ── Fortschrittsbalken ────────────────────────── */
.mine-progress-wrap {
height: 5px;
background: rgba(255,255,255,0.07);
border-radius: 3px;
margin: 8px 0 6px;
overflow: hidden;
}
.mine-progress-bar {
height: 100%;
background: linear-gradient(90deg, #c8952a, #f0c84a);
border-radius: 3px;
transition: width 0.6s ease;
}
/* ── Timer-Zeile ───────────────────────────────── */
@ -149,21 +222,117 @@
font-size: 13px;
font-family: monospace;
color: #f0c84a;
background: rgba(0, 0, 0, 0.35);
background: rgba(0,0,0,0.35);
border: 1px solid #7a5a1a;
border-radius: 5px;
padding: 3px 10px;
letter-spacing: 0.05em;
}
.mine-timer-full {
color: #ff9040;
border-color: #ff6020;
animation: minePulse 1s ease-in-out infinite;
}
/* ── Schleifen-Bereich ─────────────────────────── */
.mine-loop-row {
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 {
font-size: 12px;
color: #a07830;
}
.mine-loop-maxed {
color: #f0c84a;
font-weight: bold;
}
.mine-btn-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;
border-radius: 6px;
padding: 8px 16px;
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);
}
.mine-btn-loop:hover:not(:disabled) {
filter: brightness(1.25);
transform: translateY(-1px);
}
.mine-btn-loop:active:not(:disabled) {
transform: translateY(0);
filter: brightness(0.9);
}
.mine-btn-loop.mine-btn-disabled,
.mine-btn-loop:disabled {
background: linear-gradient(180deg, #1a1020 0%, #100810 100%);
border-color: #3a2050;
color: #604080;
cursor: not-allowed;
box-shadow: none;
transform: none;
filter: none;
}
.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: 14px;
margin-top: 4px;
display: flex;
justify-content: center;
}
/* ── Abholen-Button (wie notification-btn) ─────── */
/* ── Abholen-Button ────────────────────────────── */
.mine-btn-collect {
background: linear-gradient(180deg, #c8952a 0%, #7a5310 100%);
color: #fff8e0;
@ -175,12 +344,10 @@
font-weight: bold;
cursor: pointer;
letter-spacing: 0.04em;
transition:
filter 0.15s,
transform 0.1s;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
transition: filter 0.15s, transform 0.1s;
text-shadow: 0 1px 2px rgba(0,0,0,0.6);
width: 100%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
.mine-btn-collect:hover:not(:disabled) {
@ -215,13 +382,8 @@
}
@keyframes minePulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
.mine-error {

View File

@ -1,6 +1,7 @@
import { showNotification } from "../notification.js";
import { showNotification } from "../notification.js";
import { refreshHud } from "../hud.js";
/* ── Einstiegspunkt ─────────────────────────────── */
export async function loadMine(buildingId) {
const actionsTab = document.getElementById("tab-actions");
if (!actionsTab) return;
@ -8,6 +9,9 @@ export async function loadMine(buildingId) {
await renderMineStatus(buildingId);
}
/*
Haupt-Render-Funktion
*/
async function renderMineStatus(buildingId) {
const actionsTab = document.getElementById("tab-actions");
try {
@ -16,78 +20,25 @@ async function renderMineStatus(buildingId) {
const data = await res.json();
if (data.error) {
actionsTab.innerHTML = "<p class='mine-error'> " + data.error + "</p>";
actionsTab.innerHTML = "<p class='mine-error'>" + data.error + "</p>";
return;
}
const minutesLeft = Math.floor(data.next_cycle_in_seconds / 60);
const secondsLeft = data.next_cycle_in_seconds % 60;
const resourceRows = data.available
.map((r) => {
const icon = resourceIcon(r.resource);
const label = resourceLabel(r.resource);
return (
"<div class='mine-resource-row " +
(data.ready ? "mine-resource-ready" : "") +
"'>" +
"<span class='mine-resource-icon'>" +
icon +
"</span>" +
"<span class='mine-resource-label'>" +
label +
"</span>" +
"<span class='mine-resource-amount'>" +
r.amount +
"</span>" +
"</div>"
);
})
.join("");
actionsTab.innerHTML =
"<div class='mine-panel'>" +
"<div class='mine-header-row'>" +
"<span class='mine-level-badge'>Level " +
data.level +
"</span>" +
"<span class='mine-cycles'>" +
(data.cycles > 0 ? data.cycles + "x Zyklus abgeschlossen" : "Laeuft...") +
"</span>" +
"</div>" +
"<div class='mine-divider'></div>" +
"<p class='mine-section-title'>Abgebaut</p>" +
"<div class='mine-resources'>" +
resourceRows +
"</div>" +
"<div class='mine-divider'></div>" +
"<div class='mine-timer-row'>" +
"<span class='mine-timer-label'>Naechster Zyklus in</span>" +
"<span class='mine-timer' id='mine-countdown' data-seconds='" +
data.next_cycle_in_seconds +
"'>" +
minutesLeft +
"m " +
secondsLeft +
"s" +
"</span>" +
"</div>" +
"<div class='mine-actions'>" +
"<button class='mine-btn-collect " +
(data.ready ? "" : "mine-btn-disabled") +
"'" +
" id='mine-collect-btn'" +
" data-building='" +
buildingId +
"'" +
(data.ready ? "" : " disabled") +
">" +
(data.ready ? "Abholen" : "Noch nicht bereit") +
"</button>" +
"</div>" +
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) : "") +
"</div>";
startCountdown(buildingId);
if (data.selected_resource) {
startCountdown(buildingId, data);
}
} catch (err) {
console.error("Mine Fehler:", err);
actionsTab.innerHTML =
@ -95,13 +46,240 @@ async function renderMineStatus(buildingId) {
}
}
/*
Abschnitt: Header (Level + Zyklusinfo)
*/
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...";
}
}
return (
"<div class='mine-header-row'>" +
"<span class='mine-level-badge'>Level " + data.level + "</span>" +
"<span class='mine-cycles'>" + cycleText + "</span>" +
"</div>"
);
}
/*
Abschnitt: Ressourcen-Auswahl
*/
function renderResourceSelector(data, buildingId) {
const resources = data.production; // [{resource, amount}]
const buttons = resources.map(r => {
const isSelected = data.selected_resource === r.resource;
return (
"<button class='mine-res-btn" +
(isSelected ? " mine-res-btn-active" : "") +
"' data-resource='" + r.resource +
"' data-building='" + buildingId + "'>" +
"<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>" +
"</button>"
);
}).join("");
return (
"<p class='mine-section-title'>Ressource wählen</p>" +
"<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
*/
function renderCollectSection(data, buildingId) {
return (
"<div class='mine-actions'>" +
"<button class='mine-btn-collect" + (data.ready ? "" : " mine-btn-disabled") + "'" +
" id='mine-collect-btn'" +
" data-building='" + buildingId + "'" +
(data.ready ? "" : " disabled") + ">" +
(data.ready ? "Abholen" : "Noch nicht bereit") +
"</button>" +
"</div>"
);
}
function renderDivider() {
return "<div class='mine-divider'></div>";
}
/*
Event-Delegation: Ressource wählen
*/
document.addEventListener("click", async (e) => {
const btn = e.target.closest(".mine-res-btn");
if (!btn || btn.classList.contains("mine-res-btn-active")) return;
const resource = btn.dataset.resource;
const buildingId = btn.dataset.building;
// Alle Buttons kurz deaktivieren
document.querySelectorAll(".mine-res-btn").forEach(b => b.disabled = true);
try {
const res = await fetch("/api/mine/" + buildingId + "/select", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ resource }),
});
if (!res.ok) throw new Error("API Fehler");
const data = await res.json();
if (data.error) {
showNotification(data.error, "Mine", "⛏️");
await renderMineStatus(buildingId);
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;
}
showNotification(
"Schleife aktiviert! +" + 5 + "h Abbauzeit.\n" +
"Noch " + data.loops_available + " Schleife(n) verfügbar.",
"Mine", "💎"
);
await renderMineStatus(buildingId);
await refreshHud();
} catch (err) {
console.error("Schleife kaufen Fehler:", err);
await renderMineStatus(buildingId);
}
});
/*
Event-Delegation: Ressource 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", {
@ -113,50 +291,40 @@ document.addEventListener("click", async (e) => {
if (data.error) {
showNotification(
data.ready_in_display
? "Die Mine ist noch nicht bereit.\nBereit in: " +
data.ready_in_display
? "Noch nicht bereit.\nBereit in: " + data.ready_in_display
: data.error,
"Mine",
"⛏️",
"Mine", "⛏️"
);
await renderMineStatus(buildingId);
return;
}
const lines = data.collected
.map(
(c) =>
resourceLabel(c.resource) +
": +" +
c.amount,
)
.join("\n");
showNotification("Erfolgreich abgeholt!\n\n" + lines, "Mine", "⛏️");
const c = data.collected;
showNotification(
"Abgeholt!\n" + resourceLabel(c.resource) + ": +" + c.amount,
"Mine", "⛏️"
);
await renderMineStatus(buildingId);
await refreshHud();
} catch (err) {
console.error("Abholen Fehler:", err);
showNotification(
"Fehler beim Abholen. Bitte erneut versuchen.",
"Fehler",
"⚠️",
);
showNotification("Fehler beim Abholen. Bitte erneut versuchen.", "Fehler", "⚠️");
await renderMineStatus(buildingId);
}
});
/*
Countdown-Timer
*/
let countdownInterval = null;
function startCountdown(buildingId) {
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;
}
if (!el) { clearInterval(countdownInterval); return; }
let secs = parseInt(el.dataset.seconds, 10) - 1;
if (secs < 0) secs = 0;
@ -170,50 +338,32 @@ function startCountdown(buildingId) {
}, 1000);
}
/*
Icons & Labels
*/
function resourceIcon(resource) {
if (resource === "iron") {
return (
"<span class='mine-resource-icon-iron'>" +
"<img src='/images/items/eisen.png' alt=''>" +
"</span>"
);
}
if (resource === "gold") {
return (
"<span class='mine-resource-icon-gold'>" +
"<img src='/images/items/goldmuenze.png' alt=''>" +
"</span>"
);
}
if (resource === "wood") {
return (
"<span class='mine-resource-icon-wood'>" +
"<img src='/images/items/holz.png' alt=''>" +
"</span>"
);
}
if (resource === "stone") {
return (
"<span class='mine-resource-icon-stone'>" +
"<img src='/images/items/stein.png' alt=''>" +
"</span>"
);
}
const map = {
copper: "🟤",
silver: "⚪",
const imgMap = {
iron: "/images/items/eisen.png",
gold: "/images/items/goldmuenze.png",
wood: "/images/items/holz.png",
stone: "/images/items/stein.png",
};
return map[resource] || "📦";
if (imgMap[resource]) {
return (
"<span class='mine-resource-icon-" + resource + "'>" +
"<img src='" + imgMap[resource] + "' alt=''>" +
"</span>"
);
}
return "📦";
}
function resourceLabel(resource) {
const map = {
gold: "Gold",
copper: "Kupfer",
silver: "Silber",
iron: "Eisen",
gold: "Gold",
iron: "Eisen",
stone: "Stein",
wood: "Holz",
wood: "Holz",
};
return map[resource] || resource;
}

View File

@ -2,6 +2,14 @@ const express = require("express");
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 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" });
@ -9,104 +17,131 @@ function requireLogin(req, res, next) {
next();
}
/*
/*
HELPER: Timer sicherstellen
Legt beim allerersten Aufruf einen
Eintrag an laeuft dann fuer immer durch.
Wird NUR durch collect() zurueckgesetzt.
*/
Legt beim allerersten Aufruf einen Eintrag an.
Wird durch select() und collect() zurückgesetzt.
*/
async function ensureTimer(userBuildingId) {
const [[existing]] = await db.query(
"SELECT last_collected FROM building_collect_timer WHERE user_building_id = ?",
[userBuildingId],
[userBuildingId]
);
if (!existing) {
await db.query(
"INSERT INTO building_collect_timer (user_building_id, last_collected) VALUES (?, NOW())",
[userBuildingId],
`INSERT INTO building_collect_timer
(user_building_id, last_collected, selected_resource, loops_purchased)
VALUES (?, NOW(), NULL, 0)`,
[userBuildingId]
);
}
}
/*
/*
HELPER: Produktionsdaten laden
Gibt immer last_collected aus der DB
*/
Filtert auf die vier abbaubaren Ressourcen.
*/
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
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 = ?
`,
[userId, buildingId],
`SELECT
ub.id AS user_building_id,
ub.level,
bp.resource,
bp.amount,
bp.cycle_seconds,
bct.last_collected,
bct.selected_resource,
bct.loops_purchased
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 für diese Session
Basis 5h + je 5h pro gekaufter Schleife
*/
function calcMaxCycles(loopsPurchased, cycleSeconds) {
const maxHours = MAX_BASE_HOURS + loopsPurchased * LOOP_HOURS;
return Math.floor((maxHours * 3600) / cycleSeconds);
}
/*
GET /api/mine/:buildingId/status
*/
*/
router.get("/:buildingId/status", requireLogin, async (req, res) => {
const userId = req.session.user.id;
const userId = req.session.user.id;
const buildingId = req.params.buildingId;
try {
// user_building holen
const [[userBuilding]] = await db.query(
"SELECT id, level FROM user_buildings WHERE user_id = ? AND building_id = ?",
[userId, buildingId],
[userId, buildingId]
);
if (!userBuilding) {
return res.status(404).json({ error: "Gebaeude nicht gefunden" });
return res.status(404).json({ error: "Gebäude nicht gefunden" });
}
// Timer einmalig starten falls noch nie geoeffnet
await ensureTimer(userBuilding.id);
const rows = await loadMineData(userId, buildingId);
if (!rows.length) {
return res.status(404).json({
error: `Keine Produktionsdaten fuer Level ${userBuilding.level} gefunden. Bitte building_production Tabelle pruefen.`,
});
return res.status(404).json({ error: "Keine Produktionsdaten gefunden" });
}
const { cycle_seconds, last_collected, level } = rows[0];
const {
cycle_seconds, last_collected,
selected_resource, loops_purchased, level
} = rows[0];
const elapsed = Math.floor(
(Date.now() - new Date(last_collected).getTime()) / 1000,
const maxCycles = calcMaxCycles(loops_purchased, cycle_seconds);
const maxHours = MAX_BASE_HOURS + loops_purchased * LOOP_HOURS;
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);
// Menge der gewählten Ressource
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]
);
const cycles = Math.floor(elapsed / cycle_seconds);
const nextIn = cycle_seconds - (elapsed % cycle_seconds);
const available = rows.map((r) => ({
resource: r.resource,
amount: r.amount * cycles,
}));
const production = rows.map((r) => ({
resource: r.resource,
amount: r.amount,
}));
const gems = currency?.gems ?? 0;
res.json({
level,
cycles,
ready: cycles > 0,
available,
max_cycles: maxCycles,
max_hours: maxHours,
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,
production,
last_collected,
next_cycle_in_seconds: nextIn,
@ -118,90 +153,201 @@ router.get("/:buildingId/status", requireLogin, async (req, res) => {
}
});
/*
/*
POST /api/mine/:buildingId/select
Ressource wählen setzt Timer und Schleifen zurück
*/
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);
// Prüfen ob Ressource in building_production vorhanden
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 für dieses Gebäude nicht verfügbar" });
}
// Timer zurücksetzen, Ressource setzen, Schleifen zurücksetzen
await db.query(
`UPDATE building_collect_timer
SET selected_resource = ?, last_collected = NOW(), loops_purchased = 0
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/collect
Ressourcen gutschreiben + Timer reset
*/
Ressourcen gutschreiben + Timer vorsetzen
*/
router.post("/:buildingId/collect", requireLogin, async (req, res) => {
const userId = req.session.user.id;
const userId = req.session.user.id;
const buildingId = req.params.buildingId;
try {
// user_building holen
const [[userBuilding]] = await db.query(
"SELECT id FROM user_buildings WHERE user_id = ? AND building_id = ?",
[userId, buildingId],
[userId, buildingId]
);
if (!userBuilding) {
return res.status(404).json({ error: "Gebaeude nicht gefunden" });
return res.status(404).json({ error: "Gebäude nicht gefunden" });
}
// Timer sicherstellen (Fallback falls Status nie aufgerufen wurde)
await ensureTimer(userBuilding.id);
const rows = await loadMineData(userId, buildingId);
if (!rows.length) {
return res.status(404).json({ error: "Gebaeude nicht gefunden" });
return res.status(404).json({ error: "Gebäude nicht gefunden" });
}
const { user_building_id, cycle_seconds, last_collected } = rows[0];
const {
user_building_id, cycle_seconds, last_collected,
selected_resource, loops_purchased
} = rows[0];
const elapsed = Math.floor(
(Date.now() - new Date(last_collected).getTime()) / 1000,
);
const cycles = Math.floor(elapsed / cycle_seconds);
if (!selected_resource) {
return res.status(400).json({ error: "Keine Ressource ausgewählt" });
}
const maxCycles = calcMaxCycles(loops_purchased, 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);
if (cycles < 1) {
const waitSeconds = cycle_seconds - elapsed;
const minutes = Math.floor(waitSeconds / 60);
const seconds = waitSeconds % 60;
return res.json({
error: "Noch nichts bereit",
error: "Noch nichts bereit",
ready_in_seconds: waitSeconds,
ready_in_display: `${minutes}m ${seconds}s`,
ready_in_display: `${Math.floor(waitSeconds / 60)}m ${waitSeconds % 60}s`,
});
}
// Jede Ressource einzeln gutschreiben
const allowedResources = [
"gold",
"silver",
"copper",
"iron",
"wood",
"stone",
"gems",
];
for (const row of rows) {
if (!allowedResources.includes(row.resource)) continue;
const toAdd = row.amount * cycles;
await db.query(
`UPDATE account_currency SET \`${row.resource}\` = \`${row.resource}\` + ? WHERE account_id = ?`,
[toAdd, userId],
);
const selectedRow = rows.find(r => r.resource === selected_resource);
if (!selectedRow) {
return res.status(400).json({ error: "Ressource nicht gefunden" });
}
// Timer zuruecksetzen: last_collected um genau die abgeschlossenen
// Zyklen vorruecken Restsekunden bleiben erhalten, kein Verlust
const newLastCollected = new Date(
new Date(last_collected).getTime() + cycles * cycle_seconds * 1000,
);
const toAdd = selectedRow.amount * cycles;
await db.query(
"UPDATE building_collect_timer SET last_collected = ? WHERE user_building_id = ?",
[newLastCollected, user_building_id],
`UPDATE account_currency
SET \`${selected_resource}\` = \`${selected_resource}\` + ?
WHERE account_id = ?`,
[toAdd, userId]
);
const collected = rows.map((r) => ({
resource: r.resource,
amount: r.amount * cycles,
}));
// Timer exakt vorsetzen Restsekunden bleiben erhalten
const newLastCollected = new Date(
new Date(last_collected).getTime() + cycles * cycle_seconds * 1000
);
res.json({ success: true, cycles, collected });
// Schleifen nach dem Abholen zurücksetzen
await db.query(
`UPDATE building_collect_timer
SET last_collected = ?, loops_purchased = 0
WHERE user_building_id = ?`,
[newLastCollected, user_building_id]
);
res.json({
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,
});
} catch (err) {
console.error(err);
res.status(500).json({ error: "DB Fehler" });