From efe45c3591a43b688076a893de9a1135d53aa8d3 Mon Sep 17 00:00:00 2001 From: cay Date: Wed, 8 Apr 2026 14:07:53 +0100 Subject: [PATCH] earga --- public/css/mine.css | 236 +++++++++++++++++---- public/js/buildings/mine.js | 408 ++++++++++++++++++++++++------------ routes/mine.route.js | 366 ++++++++++++++++++++++---------- 3 files changed, 734 insertions(+), 276 deletions(-) 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 = "
" + - "
" + - "Level " + - data.level + - "" + - "" + - (data.cycles > 0 ? data.cycles + "x Zyklus abgeschlossen" : "Laeuft...") + - "" + - "
" + - "
" + - "

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 ( + "
" + + "Level " + data.level + "" + + "" + cycleText + "" + + "
" + ); +} + +/* ───────────────────────────────────────────────── + 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" });