diff --git a/public/css/mine.css b/public/css/mine.css
index 2af1f60..9925330 100644
--- a/public/css/mine.css
+++ b/public/css/mine.css
@@ -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 {
diff --git a/public/js/buildings/mine.js b/public/js/buildings/mine.js
index fb485a5..e16eb2c 100644
--- a/public/js/buildings/mine.js
+++ b/public/js/buildings/mine.js
@@ -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 = "
" + data.error + "
";
+ actionsTab.innerHTML = "" + data.error + "
";
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 (
- "" +
- "" +
- icon +
- "" +
- "" +
- label +
- "" +
- "" +
- r.amount +
- "" +
- "
"
- );
- })
- .join("");
-
actionsTab.innerHTML =
"" +
- "" +
- "
" +
- "
Abgebaut
" +
- "
" +
- resourceRows +
- "
" +
- "
" +
- "
" +
- "Naechster Zyklus in" +
- "" +
- minutesLeft +
- "m " +
- secondsLeft +
- "s" +
- "" +
- "
" +
- "
" +
- "" +
- "
" +
+ 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) : "") +
"
";
- 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 (
+ ""
+ );
+}
+
+/* ─────────────────────────────────────────────────
+ 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 (
+ ""
+ );
+ }).join("");
+
+ return (
+ "Ressource wählen
" +
+ "" +
+ buttons +
+ "
"
+ );
+}
+
+/* ─────────────────────────────────────────────────
+ 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() +
+ "Aktuelle Produktion
" +
+ "" +
+ "" + resourceIcon(data.selected_resource) + "" +
+ "" + resourceLabel(data.selected_resource) + "" +
+ "" + data.available_amount + "" +
+ "
" +
+ "" +
+ "" +
+ "" +
+ (data.is_full
+ ? "Maximale Zeit erreicht (" + maxHoursDisplay + ")"
+ : "Nächster Zyklus in") +
+ "" +
+ (data.is_full
+ ? "VOLL"
+ : "" +
+ minutesLeft + "m " + secondsLeft + "s") +
+ "
"
+ );
+}
+
+/* ─────────────────────────────────────────────────
+ Abschnitt: Schleifen (Loops)
+───────────────────────────────────────────────── */
+function renderLoopSection(data, buildingId) {
+ const loopDots = [];
+ for (let i = 0; i < 4; i++) {
+ loopDots.push(
+ ""
+ );
+ }
+
+ const canBuy = data.loops_available > 0 && data.can_afford_loop;
+ const maxReached = data.loops_available === 0;
+
+ return (
+ "Schleifen
" +
+ "" +
+ "
" + loopDots.join("") + "
" +
+ "
" +
+ data.max_hours + "h max" +
+ (maxReached ? " (Max)" : "") +
+ "" +
+ "
" +
+ (!maxReached
+ ? ""
+ : ""
+ )
+ );
+}
+
+/* ─────────────────────────────────────────────────
+ Abschnitt: Abholen-Button
+───────────────────────────────────────────────── */
+function renderCollectSection(data, buildingId) {
+ return (
+ "" +
+ "" +
+ "
"
+ );
+}
+
+function renderDivider() {
+ return "";
+}
+
+/* ─────────────────────────────────────────────────
+ 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 (
- "" +
- "
" +
- ""
- );
- }
- if (resource === "gold") {
- return (
- "" +
- "
" +
- ""
- );
- }
- if (resource === "wood") {
- return (
- "" +
- "
" +
- ""
- );
- }
- if (resource === "stone") {
- return (
- "" +
- "
" +
- ""
- );
- }
- 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 (
+ "" +
+ "
" +
+ ""
+ );
+ }
+ 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;
}
diff --git a/routes/mine.route.js b/routes/mine.route.js
index ad1ea0b..e653435 100644
--- a/routes/mine.route.js
+++ b/routes/mine.route.js
@@ -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" });