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; 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 { .mine-resources {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -68,9 +132,7 @@
border: 1px solid rgba(122, 90, 26, 0.3); border: 1px solid rgba(122, 90, 26, 0.3);
border-radius: 6px; border-radius: 6px;
padding: 7px 12px; padding: 7px 12px;
transition: transition: border-color 0.2s, background 0.2s;
border-color 0.2s,
background 0.2s;
} }
.mine-resource-row.mine-resource-ready { .mine-resource-row.mine-resource-ready {
@ -87,31 +149,27 @@
justify-content: center; justify-content: center;
} }
/* Ressource Icons (gem, silver, wood, stone, gold, iron) einheitliche Größe */ /* Ressource Icons einheitliche Größe */
.mine-resource-icon-gem,
.mine-resource-icon-silver,
.mine-resource-icon-wood,
.mine-resource-icon-stone,
.mine-resource-icon-gold, .mine-resource-icon-gold,
.mine-resource-icon-iron { .mine-resource-icon-iron,
width: 100px; .mine-resource-icon-wood,
height: 100px; .mine-resource-icon-stone {
width: 28px;
height: 28px;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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-gold img,
.mine-resource-icon-iron img { .mine-resource-icon-iron img,
width: 100px; .mine-resource-icon-wood img,
height: 100px; .mine-resource-icon-stone img {
width: 28px;
height: 28px;
object-fit: contain; 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; display: block;
} }
@ -120,7 +178,6 @@
font-size: 13px; font-size: 13px;
font-weight: bold; font-weight: bold;
color: #ffffff; color: #ffffff;
text-shadow: none;
} }
.mine-resource-amount { .mine-resource-amount {
@ -129,7 +186,23 @@
color: #f0c84a; color: #f0c84a;
min-width: 32px; min-width: 32px;
text-align: right; 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 ───────────────────────────────── */ /* ── Timer-Zeile ───────────────────────────────── */
@ -149,21 +222,117 @@
font-size: 13px; font-size: 13px;
font-family: monospace; font-family: monospace;
color: #f0c84a; color: #f0c84a;
background: rgba(0, 0, 0, 0.35); background: rgba(0,0,0,0.35);
border: 1px solid #7a5a1a; border: 1px solid #7a5a1a;
border-radius: 5px; border-radius: 5px;
padding: 3px 10px; padding: 3px 10px;
letter-spacing: 0.05em; 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 ───────────────────────────── */ /* ── Aktions-Bereich ───────────────────────────── */
.mine-actions { .mine-actions {
margin-top: 14px; margin-top: 4px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
/* ── Abholen-Button (wie notification-btn) ─────── */ /* ── Abholen-Button ────────────────────────────── */
.mine-btn-collect { .mine-btn-collect {
background: linear-gradient(180deg, #c8952a 0%, #7a5310 100%); background: linear-gradient(180deg, #c8952a 0%, #7a5310 100%);
color: #fff8e0; color: #fff8e0;
@ -175,12 +344,10 @@
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
letter-spacing: 0.04em; letter-spacing: 0.04em;
transition: transition: filter 0.15s, transform 0.1s;
filter 0.15s, text-shadow: 0 1px 2px rgba(0,0,0,0.6);
transform 0.1s;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
width: 100%; 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) { .mine-btn-collect:hover:not(:disabled) {
@ -215,13 +382,8 @@
} }
@keyframes minePulse { @keyframes minePulse {
0%, 0%, 100% { opacity: 1; }
100% { 50% { opacity: 0.45; }
opacity: 1;
}
50% {
opacity: 0.45;
}
} }
.mine-error { .mine-error {

View File

@ -1,6 +1,7 @@
import { showNotification } from "../notification.js"; import { showNotification } from "../notification.js";
import { refreshHud } from "../hud.js"; import { refreshHud } from "../hud.js";
/* ── Einstiegspunkt ─────────────────────────────── */
export async function loadMine(buildingId) { export async function loadMine(buildingId) {
const actionsTab = document.getElementById("tab-actions"); const actionsTab = document.getElementById("tab-actions");
if (!actionsTab) return; if (!actionsTab) return;
@ -8,6 +9,9 @@ export async function loadMine(buildingId) {
await renderMineStatus(buildingId); await renderMineStatus(buildingId);
} }
/*
Haupt-Render-Funktion
*/
async function renderMineStatus(buildingId) { async function renderMineStatus(buildingId) {
const actionsTab = document.getElementById("tab-actions"); const actionsTab = document.getElementById("tab-actions");
try { try {
@ -16,78 +20,25 @@ async function renderMineStatus(buildingId) {
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
actionsTab.innerHTML = "<p class='mine-error'> " + data.error + "</p>"; actionsTab.innerHTML = "<p class='mine-error'>" + data.error + "</p>";
return; 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 = actionsTab.innerHTML =
"<div class='mine-panel'>" + "<div class='mine-panel'>" +
"<div class='mine-header-row'>" + renderHeader(data) +
"<span class='mine-level-badge'>Level " + renderDivider() +
data.level + renderResourceSelector(data, buildingId) +
"</span>" + (data.selected_resource ? renderProductionSection(data) : "") +
"<span class='mine-cycles'>" + (data.selected_resource ? renderDivider() : "") +
(data.cycles > 0 ? data.cycles + "x Zyklus abgeschlossen" : "Laeuft...") + (data.selected_resource ? renderLoopSection(data, buildingId) : "") +
"</span>" + (data.selected_resource ? renderDivider() : "") +
"</div>" + (data.selected_resource ? renderCollectSection(data, buildingId) : "") +
"<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>" +
"</div>"; "</div>";
startCountdown(buildingId); if (data.selected_resource) {
startCountdown(buildingId, data);
}
} catch (err) { } catch (err) {
console.error("Mine Fehler:", err); console.error("Mine Fehler:", err);
actionsTab.innerHTML = 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) => { document.addEventListener("click", async (e) => {
const btn = e.target.closest("#mine-collect-btn"); const btn = e.target.closest("#mine-collect-btn");
if (!btn || btn.disabled) return; if (!btn || btn.disabled) return;
const buildingId = btn.dataset.building; const buildingId = btn.dataset.building;
btn.disabled = true; btn.disabled = true;
btn.textContent = "Wird abgeholt..."; btn.textContent = "Wird abgeholt...";
try { try {
const res = await fetch("/api/mine/" + buildingId + "/collect", { const res = await fetch("/api/mine/" + buildingId + "/collect", {
@ -113,50 +291,40 @@ document.addEventListener("click", async (e) => {
if (data.error) { if (data.error) {
showNotification( showNotification(
data.ready_in_display data.ready_in_display
? "Die Mine ist noch nicht bereit.\nBereit in: " + ? "Noch nicht bereit.\nBereit in: " + data.ready_in_display
data.ready_in_display
: data.error, : data.error,
"Mine", "Mine", "⛏️"
"⛏️",
); );
await renderMineStatus(buildingId); await renderMineStatus(buildingId);
return; return;
} }
const lines = data.collected const c = data.collected;
.map( showNotification(
(c) => "Abgeholt!\n" + resourceLabel(c.resource) + ": +" + c.amount,
resourceLabel(c.resource) + "Mine", "⛏️"
": +" + );
c.amount,
)
.join("\n");
showNotification("Erfolgreich abgeholt!\n\n" + lines, "Mine", "⛏️");
await renderMineStatus(buildingId); await renderMineStatus(buildingId);
await refreshHud(); await refreshHud();
} catch (err) { } catch (err) {
console.error("Abholen Fehler:", err); console.error("Abholen Fehler:", err);
showNotification( showNotification("Fehler beim Abholen. Bitte erneut versuchen.", "Fehler", "⚠️");
"Fehler beim Abholen. Bitte erneut versuchen.",
"Fehler",
"⚠️",
);
await renderMineStatus(buildingId); await renderMineStatus(buildingId);
} }
}); });
/*
Countdown-Timer
*/
let countdownInterval = null; let countdownInterval = null;
function startCountdown(buildingId) { function startCountdown(buildingId, data) {
if (countdownInterval) clearInterval(countdownInterval); if (countdownInterval) clearInterval(countdownInterval);
if (data.is_full) return;
countdownInterval = setInterval(() => { countdownInterval = setInterval(() => {
const el = document.getElementById("mine-countdown"); const el = document.getElementById("mine-countdown");
if (!el) { if (!el) { clearInterval(countdownInterval); return; }
clearInterval(countdownInterval);
return;
}
let secs = parseInt(el.dataset.seconds, 10) - 1; let secs = parseInt(el.dataset.seconds, 10) - 1;
if (secs < 0) secs = 0; if (secs < 0) secs = 0;
@ -170,50 +338,32 @@ function startCountdown(buildingId) {
}, 1000); }, 1000);
} }
/*
Icons & Labels
*/
function resourceIcon(resource) { function resourceIcon(resource) {
if (resource === "iron") { const imgMap = {
return ( iron: "/images/items/eisen.png",
"<span class='mine-resource-icon-iron'>" + gold: "/images/items/goldmuenze.png",
"<img src='/images/items/eisen.png' alt=''>" + wood: "/images/items/holz.png",
"</span>" stone: "/images/items/stein.png",
);
}
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: "⚪",
}; };
return map[resource] || "📦"; if (imgMap[resource]) {
return (
"<span class='mine-resource-icon-" + resource + "'>" +
"<img src='" + imgMap[resource] + "' alt=''>" +
"</span>"
);
}
return "📦";
} }
function resourceLabel(resource) { function resourceLabel(resource) {
const map = { const map = {
gold: "Gold", gold: "Gold",
copper: "Kupfer", iron: "Eisen",
silver: "Silber",
iron: "Eisen",
stone: "Stein", stone: "Stein",
wood: "Holz", wood: "Holz",
}; };
return map[resource] || resource; return map[resource] || resource;
} }

View File

@ -2,6 +2,14 @@ const express = require("express");
const router = require("express").Router(); const router = require("express").Router();
const db = require("../database/database"); 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) { function requireLogin(req, res, next) {
if (!req.session?.user) { if (!req.session?.user) {
return res.status(401).json({ error: "Nicht eingeloggt" }); return res.status(401).json({ error: "Nicht eingeloggt" });
@ -9,104 +17,131 @@ function requireLogin(req, res, next) {
next(); next();
} }
/* /*
HELPER: Timer sicherstellen HELPER: Timer sicherstellen
Legt beim allerersten Aufruf einen Legt beim allerersten Aufruf einen Eintrag an.
Eintrag an laeuft dann fuer immer durch. Wird durch select() und collect() zurückgesetzt.
Wird NUR durch collect() zurueckgesetzt. */
*/
async function ensureTimer(userBuildingId) { async function ensureTimer(userBuildingId) {
const [[existing]] = await db.query( const [[existing]] = await db.query(
"SELECT last_collected FROM building_collect_timer WHERE user_building_id = ?", "SELECT last_collected FROM building_collect_timer WHERE user_building_id = ?",
[userBuildingId], [userBuildingId]
); );
if (!existing) { if (!existing) {
await db.query( await db.query(
"INSERT INTO building_collect_timer (user_building_id, last_collected) VALUES (?, NOW())", `INSERT INTO building_collect_timer
[userBuildingId], (user_building_id, last_collected, selected_resource, loops_purchased)
VALUES (?, NOW(), NULL, 0)`,
[userBuildingId]
); );
} }
} }
/* /*
HELPER: Produktionsdaten laden HELPER: Produktionsdaten laden
Gibt immer last_collected aus der DB Filtert auf die vier abbaubaren Ressourcen.
*/ */
async function loadMineData(userId, buildingId) { async function loadMineData(userId, buildingId) {
const [rows] = await db.query( const [rows] = await db.query(
` `SELECT
SELECT ub.id AS user_building_id,
ub.id AS user_building_id, ub.level,
ub.level, bp.resource,
bp.resource, bp.amount,
bp.amount, bp.cycle_seconds,
bp.cycle_seconds, bct.last_collected,
bct.last_collected bct.selected_resource,
FROM user_buildings ub bct.loops_purchased
JOIN building_production bp FROM user_buildings ub
ON bp.building_id = ub.building_id JOIN building_production bp
AND bp.level = ub.level ON bp.building_id = ub.building_id
JOIN building_collect_timer bct AND bp.level = ub.level
ON bct.user_building_id = ub.id JOIN building_collect_timer bct
WHERE ub.user_id = ? ON bct.user_building_id = ub.id
AND ub.building_id = ? WHERE ub.user_id = ?
`, AND ub.building_id = ?
[userId, buildingId], AND bp.resource IN ('gold','iron','stone','wood')`,
[userId, buildingId]
); );
return rows; 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 GET /api/mine/:buildingId/status
*/ */
router.get("/:buildingId/status", requireLogin, async (req, res) => { 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; const buildingId = req.params.buildingId;
try { try {
// user_building holen
const [[userBuilding]] = await db.query( const [[userBuilding]] = await db.query(
"SELECT id, level FROM user_buildings WHERE user_id = ? AND building_id = ?", "SELECT id, level FROM user_buildings WHERE user_id = ? AND building_id = ?",
[userId, buildingId], [userId, buildingId]
); );
if (!userBuilding) { 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); await ensureTimer(userBuilding.id);
const rows = await loadMineData(userId, buildingId); const rows = await loadMineData(userId, buildingId);
if (!rows.length) { if (!rows.length) {
return res.status(404).json({ return res.status(404).json({ error: "Keine Produktionsdaten gefunden" });
error: `Keine Produktionsdaten fuer Level ${userBuilding.level} gefunden. Bitte building_production Tabelle pruefen.`,
});
} }
const { cycle_seconds, last_collected, level } = rows[0]; const {
cycle_seconds, last_collected,
selected_resource, loops_purchased, level
} = rows[0];
const elapsed = Math.floor( const maxCycles = calcMaxCycles(loops_purchased, cycle_seconds);
(Date.now() - new Date(last_collected).getTime()) / 1000, 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 gems = currency?.gems ?? 0;
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,
}));
res.json({ res.json({
level, level,
cycles, cycles,
ready: cycles > 0, max_cycles: maxCycles,
available, 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, production,
last_collected, last_collected,
next_cycle_in_seconds: nextIn, 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 POST /api/mine/:buildingId/collect
Ressourcen gutschreiben + Timer reset Ressourcen gutschreiben + Timer vorsetzen
*/ */
router.post("/:buildingId/collect", requireLogin, async (req, res) => { 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; const buildingId = req.params.buildingId;
try { try {
// user_building holen
const [[userBuilding]] = await db.query( const [[userBuilding]] = await db.query(
"SELECT id FROM user_buildings WHERE user_id = ? AND building_id = ?", "SELECT id FROM user_buildings WHERE user_id = ? AND building_id = ?",
[userId, buildingId], [userId, buildingId]
); );
if (!userBuilding) { 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); await ensureTimer(userBuilding.id);
const rows = await loadMineData(userId, buildingId); const rows = await loadMineData(userId, buildingId);
if (!rows.length) { 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( if (!selected_resource) {
(Date.now() - new Date(last_collected).getTime()) / 1000, return res.status(400).json({ error: "Keine Ressource ausgewählt" });
); }
const cycles = Math.floor(elapsed / cycle_seconds);
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) { if (cycles < 1) {
const waitSeconds = cycle_seconds - elapsed; const waitSeconds = cycle_seconds - elapsed;
const minutes = Math.floor(waitSeconds / 60);
const seconds = waitSeconds % 60;
return res.json({ return res.json({
error: "Noch nichts bereit", error: "Noch nichts bereit",
ready_in_seconds: waitSeconds, 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 selectedRow = rows.find(r => r.resource === selected_resource);
const allowedResources = [ if (!selectedRow) {
"gold", return res.status(400).json({ error: "Ressource nicht gefunden" });
"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],
);
} }
// Timer zuruecksetzen: last_collected um genau die abgeschlossenen const toAdd = selectedRow.amount * cycles;
// Zyklen vorruecken Restsekunden bleiben erhalten, kein Verlust
const newLastCollected = new Date(
new Date(last_collected).getTime() + cycles * cycle_seconds * 1000,
);
await db.query( await db.query(
"UPDATE building_collect_timer SET last_collected = ? WHERE user_building_id = ?", `UPDATE account_currency
[newLastCollected, user_building_id], SET \`${selected_resource}\` = \`${selected_resource}\` + ?
WHERE account_id = ?`,
[toAdd, userId]
); );
const collected = rows.map((r) => ({ // Timer exakt vorsetzen Restsekunden bleiben erhalten
resource: r.resource, const newLastCollected = new Date(
amount: r.amount * cycles, 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) { } catch (err) {
console.error(err); console.error(err);
res.status(500).json({ error: "DB Fehler" }); res.status(500).json({ error: "DB Fehler" });